/*
################################################################################
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'
            }
        },
        $elements = {},
        $window, $document, $body;

    _$elements = $elements;

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

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

    /**
        Creates an App instance.
    */
    Ox.App = function() {
        return function(options) {

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

            self.time = +new Date();

            self.options = $.extend({
                apiTimeout: 15000,
                apiType: 'POST',
                apiURL: '',
                config: '',
                init: ''
            }, options);

            function getUserAgent() {
                var userAgent = '';
                $.each(['Chrome', 'Firefox', 'Internet Explorer', 'Opera', 'Safari'], function(i, v) {
                    if (navigator.userAgent.indexOf(v) > -1) {
                        userAgent = v;
                        return false;
                    }
                });
                return userAgent;
            }

            function getUserData() {
                //return {};
                return {};
                return {
                    navigator: {
                        cookieEnabled: navigator.cookieEnabled,
                        plugins: $.map(navigator.plugins, function(plugin, i) {
                            return plugin.name;
                        }),
                        userAgent: navigator.userAgent
                    },
                    screen: screen,
                    time: (+new Date() - self.time) / 1000,
                    window: {
                        innerHeight: window.innerHeight,
                        innerWidth: window.innerWidth,
                        outerHeight: window.outerHeight,
                        outerWidth: window.outerWidth,
                        screenLeft: window.screenLeft,
                        screenTop: window.screenTop
                    }
                };
            }

            function loadImages(callback) {
                $.getJSON(oxui.path + 'json/ox.ui.images.json', function(data) {
                    var counter = 0,
                        length = data.length;
                    data.forEach(function(src, i) {
                        image = new Image()
                        image.src = oxui.path + src;
                        image.onload = function() {
                            (++counter == length) && callback();
                        }
                    });
                });
            }

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

            that.api = {
                api: function(callback) {
                    Ox.Request.send({
                        url: self.options.apiURL,
                        data: {
                            action: 'api'
                        },
                        callback: callback
                    });
                },
                cancel: function(id) {
                    Ox.Request.cancel(id);
                }
            };

            that.launch = function(callback) {
                var time = +new Date(),
                    userAgent = getUserAgent(),
                    userAgents = ['Chrome', 'Firefox', 'Opera', 'Safari'];
                $.ajaxSetup({
                    timeout: self.options.apiTimeout,
                    type: self.options.apiType,
                    url: self.options.apiURL
                });
                userAgents.indexOf(userAgent) > -1 ? start() : stop();
                function start() {
                    $.getJSON(self.options.config, function(data) {
                        var config = data;
                        Ox.print('config', config);
                        document.title = config.site.name;
                        loadImages(function() {
                            that.api.api(function(result) {
                                $.each(result.data.actions, function(i, action) {
                                    that.api[action] = function(data, callback) {
                                        if (arguments.length == 1 && Ox.isFunction(data)) {
                                            callback = data;
                                            data = {};
                                        }
                                        return Ox.Request.send({
                                            url: self.options.apiURL,
                                            data: {
                                                action: action,
                                                data: JSON.stringify(data)
                                            },
                                            callback: callback
                                        });
                                    };
                                });
                                that.api[self.options.init](getUserData(), function(data) {
                                    var user = data.data.user;
                                    $(function() {
                                        var $div = $body.find('div');
                                        $body.find('img').remove();
                                        $div.animate({
                                            opacity: 0
                                        }, 1000, function() {
                                            $div.remove();
                                        });
                                        callback({
                                            config: config,
                                            user: user
                                        });
                                    });
                                });
                            });
                        });
                    });
                }
                function stop() {
                    that.request.send(self.options.init, getUserData(), function() {});
                }
                return that;
            };

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

            return that;

        };

    }();

    /**
    Ox.Event event object
    */
    /*
    Ox.Event = function() {
        var $eventHandler = $('<div>'),
            events = {};
        function bindEvent(id, event, callback) {
            Ox.print('bind', id, event);
            events[id] = events[id] || {};
            events[id][event] = events[id][event] || [];
            if ($.map(events[id][event], function(fn) {
                return fn.toString();
            }).indexOf(callback.toString()) == -1) {
                events[id][event].push(callback);
                if (!isKeyboardEvent(event) || Ox.Focus.focused() == id) {
                    $eventHandler.bind(event + '_' + id, callback); // requestStart/requestStop currently have '' as id
                }
            }
        }
        function unbindEvent(id, event, callback) {
            Ox.print('unbind', id, event);
            var toString = (callback || '').toString(),
                unbind = !event || (event && !isKeyboardEvent(event)) || Ox.Focus.focused() == id;
            if (events[id] && (!event || events[id][event])) {
                $.each(events[id], function(e, fns) {
                    if (!event || event == e) {
                        events[id][e] = $.map(events[id][e], function(fn, i) {
                            if (!callback || toString == fn.toString()) {
                                Ox.print(unbind, 'eventHandler.unbind', e, id)
                                unbind && $eventHandler.unbind(e + '_' + id, fn);
                                return null;
                            } else {
                                return fn;
                            }
                        });
                    }
                });
                Ox.print('id/events', id, events, events[id])
                if (!callback || (event && events[id][event].length == 0)) {
                    delete events[id][event];
                }
                if (!event || Ox.length(events[id]) == 0) {
                    delete events[id];
                }
            }
        }
        function isKeyboardEvent(event) {
            return event.substr(0, 4) == 'key_';
        }
        return {
            _print: function() {
                Ox.print(events);
            },
            bind: function(id, event, callback) {
                // bind event
                bindEvent(id, event, callback);
            },
            bindKeyboard: function(id) {
                // bind all keyboard events
                //Ox.print('binding', 'id', id, 'events', events[id], Ox.length(events[id]), 'keyboardevents', events[id]['keyboard'])
                $.each(events[id], function(k, v) {
                    Ox.print('|' + k + '|');
                })
                events[id] && $.each(events[id], function(event, callbacks) {
                    Ox.print('BIND ID EVENT', id, event)
                    isKeyboardEvent(event) && $.each(callbacks, function(i, callback) {
                        Ox.print('bind', id, event);
                        $eventHandler.bind(event, callback);
                    });
                });
            },
            changeId: function(oldId, newId) {
                // we need to reference events by options('id'),
                // not by that.id, since textlist events fire on list,
                // new lists have new that.ids, etc.
                // so this renaming is necessary
                Ox.print('changeId', oldId, newId, events[oldId]);
                $.each($.extend({}, events[oldId] || {}), function(event, callbacks) {
                    $.each(callbacks, function(i, callback) {
                        Ox.Event.unbind(oldId, event, callback);
                        Ox.Event.bind(newId, event, callback);
                    });
                });
            },
            unbind: function(id, event, callback) {
                // unbind event
                // event and callback are optional
                unbindEvent(id, event, callback);
            },
            unbindAll: function() {
                Ox.print('beginUnbindAll')
                $.each(events, function(id, eventsById) {
                    $.each(eventsById, function(event, callbacks) {
                        $.each(callbacks, function(i, callback) {
                            Ox.Event.unbind(id, event, callback);
                        });
                    });
                });
                Ox.print('endUnbindAll')
            },
            trigger: function(id, event, data) {
                // trigger event
                // data is optional
                // keyboard handler will call this with '' as id
                Ox.print('trigger', id, event, data || {});
                $eventHandler.trigger(event + (id ? '_' + id : ''), data || {});
            },
            unbindKeyboard: function(id) {
                // unbind all keyboard events
                Ox.print('unbinding', id)
                events[id] && $.each(events[id], function(event, callbacks) {
                    Ox.print('UNBIND ID EVENT', id, event)
                    isKeyboardEvent(event) && $.each(callbacks, function(i, callback) {
                        Ox.print('unbind', event)
                        $eventHandler.unbind(event, callback);
                    });
                });
            }
        }
    }();
    */

    /**
        Ox.Focus
    */

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

    /**
        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 focused = Ox.Focus.focused(),
                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;
            }
            focused && $elements[focused].triggerEvent('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 request object
    */
    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 && $body.trigger('requestStop');
                    options.callback && 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: [
                                    new Ox.Button({
                                        title: 'Close'
                                    })
                                    .bindEvent({
                                        click: function() {
                                            $dialog.close();
                                        }
                                    })
                                ],
                                content: $iframe,
                                width: 800,
                                height: 400
                            })
                            .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) {
                            try {
                                data = {
                                    status: {
                                        code: request.status,
                                        text: request.statusText
                                    }
                                };
                            } catch (err) {
                                data = {
                                    status: {
                                        code: '500',
                                        text: 'Unknown Error'
                                    }
                                };
                            }
                        }
                    }
                    if (data.status.code < 500) {
                        callback(data);
                    } else {
                        var $dialog = new Ox.Dialog({
                                title: 'Application Error',
                                buttons: [
                                    new Ox.Button({
                                        title: 'Details'
                                    })
                                    .bindEvent({
                                        click: function() {
                                            $dialog.close(function() {
                                                debug(request);
                                            })
                                        }
                                    }),
                                    new Ox.Button({
                                        title: 'Close'
                                    })
                                    .bindEvent({
                                        click: function() {
                                            $dialog.close();
                                        }
                                    })
                                ],
                                content: '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.',
                                width: 400,
                                height: 200
                            })
                            .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;
                    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,
                            dataType: 'json',
                            error: error,
                            success: success,
                            timeout: options.timeout,
                            type: options.type,
                            url: options.url
                        });
                        Ox.print('request', options.data, Ox.length(requests));
                        Ox.length(requests) == 1 && $body.trigger('requestStart');
                    }
                }

                return options.id;

            },

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

        };
    }();

    /**
     not implemented
    */
    Ox.Theme = function() {
        
    };

    /**
    Ox.URL
    */

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


    // 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('div', self)
            .options(options || {})
            .addClass('OxContainer');
        that.$content = new Ox.Element('div', self)
            .options(options || {})
            .addClass('OxContent')
            .appendTo(that);
        return that;
    };


    // check out http://ejohn.org/apps/learn/#36 (-#38, making fns work w/o new)
    /**
    Ox.Element
    */
    Ox.Element = function() {

        return function(options, self) {

            // construct
            self = self || {};
            self.options = options || {};
            if (!self.$eventHandler) {
                self.$eventHandler = $('<div>');
            }
            var that = this;

            // init
            (function() {
                // allow for Ox.Element('tagname', self)
                if (typeof self.options == 'string') {
                    self.options = {
                        element: self.options
                    };
                }
                that.ox = Ox.version;
                that.id = Ox.uid();
                that.$element = $('<' + (self.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 (Ox.isUndefined(arg)) {
                                Ox.print('fn', fn, 'undefined argument')
                            }
                            // if an ox object was passed
                            // then pass its $element instead
                            // so we can do oxObj.jqFn(oxObj)
                            if (arg && 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? (that?)
                        // ret = that.$element[fn].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() {
                /*
                binds a function to an event triggered by this object
                bindEvent(event, fn) or bindEvent({event0: fn0, event1: fn1, ...})
                */
                if (arguments.length == 1) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.print(that.id, 'bind', event);
                        self.$eventHandler.bind('ox_' + event, fn);
                    });
                } else {
                    Ox.print(that.id, 'bind', arguments[0]);
                    self.$eventHandler.bind('ox_' + 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 args,
                    length = arguments.length,
                    oldOptions,
                    ret;
                if (length == 0) {
                    // options()
                    ret = self.options;
                } else if (length == 1 && typeof arguments[0] == 'string') {
                    // options(str)
                    ret = self.options ? self.options[arguments[0]] : options[arguments[0]];
                } else {
                    // options (str, val) or options({str: val, ...})
                    // translate (str, val) to ({str: val})
                    args = Ox.makeObject.apply(that, arguments || {});
                    oldOptions = $.extend({}, self.options);
                    // if options have not been set, extend defaults,
                    // otherwise, extend options
                    //self.options = $.extend(self.options, self.options ? {} : self.defaults, args);
                    self.options = $.extend({}, self.defaults, self.options, args);
                    //self.options = $.extend(self.options || self.defaults, args);
                    $.each(args, function(key, value) {
                        // key == 'id' && id && Ox.Event.changeId(id, value);
                        /*!Ox.equals(value, oldOptions[key]) &&*/ self.onChange(key, value);
                    });
                    ret = that;
                }
                return ret;
            };

            that.remove = function() { // fixme: clashes with jquery, should be removeElement
                that.loseFocus();
                delete self.$eventHandler;
                that.$element.remove();
                delete $elements[that.ox];
                return that;
            };

            that.triggerEvent = function() {
                /*
                triggers an event
                triggerEvent(event) or triggerEvent(event, data) or triggerEvent({event0: data, event1: data, ...})
                */
                if (Ox.isObject(arguments[0])) {
                    $.each(arguments[0], function(event, data) {
                        Ox.print(that.id, self.options.id, 'trigger', event, data);
                        self.$eventHandler.trigger('ox_' + event, data);
                    });
                } else {
                    Ox.print('??', that.id, self.options ? self.options.id : '', 'trigger', arguments[0], arguments[1] || {});
                    self.$eventHandler.trigger('ox_' + arguments[0], arguments[1] || {});
                }
                return that;
            };

            that.unbindEvent = function() {
                /*
                unbinds an event
                unbindEvent(event, fn) or unbindEvent({event0: fn0, event1: fn1, ...})
                */
                if (arguments.length == 1) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.print(that.id, 'unbind', arguments[0]);
                        self.$eventHandler.unbind('ox_' + event, fn);
                    });
                } else {
                    Ox.print(that.id, 'unbind', arguments[0]);
                    self.$eventHandler.unbind('ox_' + 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.Window
    */
    Ox.Window = function(options, self) {

        self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    draggable: true,
                    fullscreenable: true, // fixme: silly name
                    height: 225,
                    resizeable: true,
                    scaleable: true,
                    width: 400
                })
                .options(options || {})

        self.center = function() {
            
        };

        self.drag = function() {
            
        };

        self.fullscreen = function() {
            
        };

        self.onChange = function() {
            
        };

        self.reset = function() {
            
        };

        self.resize = function() {
            
        };

        self.scale = function() {
            
        };

        that.close = function() {
            
        };

        that.open = function() {
            
        };

        return that;

    };


    // fixme: this should be Ox.Theme, and provide Ox.Theme.set(), Ox.Theme.load, etc.
    /**
        if name is given as argument, switch to this theme.
        return current theme otherwise.

        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;
    };

    /**
    */
    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.Resizebar = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    collapsed: false,
                    collapsible: true,
                    edge: 'left',
                    elements: [],
                    orientation: 'horizontal',
                    parent: null,
                    resizable: true,
                    resize: [],
                    size: 0
                })
                .options(options || {}) // fixme: options function should be able to handle undefined, no need for || {}
                .addClass('OxResizebar Ox' + Ox.toTitleCase(self.options.orientation))
                /*
                .attr({
                    draggable: 'true'
                })
                .bind('dragstart', function(e) {
                    // e.originalEvent.dataTransfer.setDragImage($('<div>')[0], 0, 0);
                })
                .bind('drag', function(e) {
                    Ox.print('dragging', e)
                })
                */
                .mousedown(dragStart)
                .dblclick(toggle)
                .append($('<div>').addClass('OxSpace'))
                .append($('<div>').addClass('OxLine'))
                .append($('<div>').addClass('OxSpace'));

        $.extend(self, {
            clientXY: self.options.orientation == 'horizontal' ? 'clientY' : 'clientX',
            dimensions: oxui.getDimensions(self.options.orientation), // fixme: should orientation be the opposite orientation here?
            edges: oxui.getEdges(self.options.orientation),
            leftOrTop: self.options.edge == 'left' || self.options.edge == 'top',
            startPos: 0,
            startSize: 0
        });

        function drag(e) {
            var d = e[self.clientXY] - self.startPos
                size = self.options.size;
            self.options.size = Ox.limit(
                self.startSize + d * (self.leftOrTop ? 1 : -1),
                self.options.resize[0],
                self.options.resize[self.options.resize.length - 1]
            );
            $.each(self.options.resize, function(i, v) {
                if (self.options.size >= v - 8 && self.options.size <= v + 8) {
                    self.options.size = v;
                    return false;
                }
            });
            if (self.options.size != size) {
                that.css(self.edges[self.leftOrTop ? 2 : 3], self.options.size + 'px');
                // fixme: send {size: x}, not x
                if (self.leftOrTop) {
                    self.options.elements[0]
                        .css(self.dimensions[1], self.options.size + 'px')
                        .triggerEvent('resize', self.options.size);
                    self.options.elements[1]
                        .css(self.edges[2], (self.options.size + 1) + 'px')
                        .triggerEvent('resize', self.options.elements[1][self.dimensions[1]]());
                    self.options.parent.updateSize(0, self.options.size);
                } else {
                    self.options.elements[0]
                        .css(self.edges[3], (self.options.size + 1) + 'px')
                        .triggerEvent('resize', self.options.elements[0][self.dimensions[1]]());
                    self.options.elements[1]
                        .css(self.dimensions[1], self.options.size + 'px')
                        .triggerEvent('resize', self.options.size);
                    self.options.parent.updateSize(1, self.options.size); // fixme: listen to event instead?
                }
            }
        }

        function dragStart(e) {
			if (self.options.resizable && !self.options.collapsed) {
				self.startPos = e[self.clientXY];
	            self.startSize = self.options.size;
	            Ox.print('startSize', self.startSize)
	            $window.mousemove(drag);
	            $window.one('mouseup', dragStop);
			}
        }

        function dragStop() {
            $window.unbind('mousemove');
        }

        function toggle() {
            if (self.options.collapsible) {
                // fixme: silly, pass a parameter
    			self.options.parent.toggle(self.leftOrTop ? 0 : self.options.parent.options('elements').length - 1);
    			self.options.collapsed = !self.options.collapsed;
            }
			/*
            Ox.print('toggle');
            if (Ox.isUndefined(self.options.position)) {
                self.options.position = parseInt(self.options.parent.css(self.options.edge)) +
                        (self.options.collapsed ? self.options.size : 0);
            }
            var size = self.options.position -
                        (self.options.collapsed ? 0 : self.options.size),
                animate = {};
            Ox.print('s.o.e', self.options.edge);
            animate[self.options.edge] = size;
            self.options.parent.animate(animate, 200, function() {
                var i = (self.options.edge == 'left' || self.options.edge == 'top') ? 0 : 1;
                self.options.collapsed = !self.options.collapsed;
				Ox.Event.trigger(self.ids[i], 'toggle', self.options.collapsed);
                Ox.Event.trigger(self.ids[1 - i], 'resize', self.options.elements[1 - i][self.dimensions[1]]());
            });
			*/
        }

        return that;

    };

    /**
    */
    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 = function(options, self) {

        // fixme: dialog should be derived from a generic draggable
        // fixme: buttons should have a close attribute, or the dialog a close id
        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    title: '',
                    buttons: [],
                    content: null,
                    height: 216,
                    keys: {},
                    minHeight: 144,
                    minWidth: 256,
                    movable: true,
                    padding: 16,
                    resizable: true,
                    width: 384
                })
                .options(options || {})
                .addClass('OxDialog')
                .bindEvent({
                    key_enter: function() {
                        keypress('enter');
                    },
                    key_escape: function() {
                        Ox.print('KEY ESCAPE')
                        keypress('escape');
                    }
                });

        $.extend(self, {
            initialWidth: self.options.width,
            initialHeight: self.options.height
        })

        that.$titlebar = new Ox.Bar({
                size: 'medium'
            })
            .addClass('OxTitleBar')
            .appendTo(that);
        self.options.movable && that.$titlebar
            .mousedown(drag)
            .dblclick(center);

        that.$title = new Ox.Element()
            .addClass('OxTitle')
            .html(self.options.title)
            .appendTo(that.$titlebar);

        // fixme: should the following be a container?
        that.$content = new Ox.Element()
            .addClass('OxContent')
            .css({
                padding: self.options.padding + 'px',
                overflow: 'auto'
            })
            .append(self.options.content)
            .appendTo(that);

        that.$buttonsbar = new Ox.Bar({})
            .addClass('OxButtonsBar')
            .appendTo(that);
        loadButtons();

        //that.$buttons[0].focus();

        that.$layer = new Ox.Element() // fixme: Layer widget that would handle click?
            .addClass('OxLayer')
            .mousedown(mousedownLayer)
            .mouseup(mouseupLayer);

        function center() {
            var documentHeight = $document.height();
            that.css({
                left: 0,
                top: Math.max(parseInt(-documentHeight / 10), self.options.height - documentHeight + 40) + 'px',
                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) {
                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');
            });
        }

        function getButtonById(id) {
            var ret = null;
            Ox.print('that.$buttons', that.$buttons, id)
            $.each(that.$buttons, function(i, button) {
                if (button.options('id') == id) {
                    ret = button;
                    return false;
                }
            });
            return ret;
        }

        function keypress(key) {
            var id = self.options.keys[key];
            Ox.print('X', key, self.options.keys)
            id && getButtonById(id).$element.trigger('click');
        }

        function loadButtons() {
            Ox.print('loadButtons', $.map(self.options.buttons, function(v) {
                return v;
            }));
            if (that.$buttons) {
                that.$buttons.forEach(function($button) {
                    $button.remove();
                });
                that.$resize.remove();
                // that.$buttonsbar.empty();
            }
            that.$buttons = [];
            if (!Ox.isArray(self.options.buttons[0])) {
                self.options.buttons = [[], self.options.buttons];
            }
            Ox.print('--- one', self.options.buttons[0]);
            $.each(self.options.buttons[0], function(i, button) {
                // Ox.print('---', button, self.options.buttons)
                that.$buttons[i] = button
                    .addClass('OxLeft')
                    .appendTo(that.$buttonsbar);
            });
            if (self.options.resizable) {
                that.$resize = new Ox.Element()
                    .addClass('OxResize')
                    .mousedown(resize)
                    .dblclick(reset)
                    .appendTo(that.$buttonsbar);
            }
            Ox.print('--- two', self.options.buttons[1]);
            $.each(self.options.buttons[1].reverse(), function(i, button) {
                //Ox.print('---', button, self.options.buttons)
                that.$buttons[that.$buttons.length] = button
                    .addClass('OxRight')
                    .appendTo(that.$buttonsbar);
            });
        }

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

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

        function reset() {
            $.extend(self.options, {
                height: self.initialHeight,
                width: self.initialWidth
            });
            that/*.css({
                    left: Math.max(that.offset().left, 24 - that.width())
                })*/
                .width(self.options.width)
                .height(self.options.height);
            that.$content.height(self.options.height - 48 - 2 * self.options.padding); // fixme: this should happen automatically
            triggerResizeEvent();
        }

        function resize(event) { // fixme: reserved jquery string?
            var 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) {
                that.css({
                    left: offset.left,
                    top: offset.top,
                    margin: 0
                });
                self.options.width = Ox.limit(
                    elementWidth - x + event.clientX,
                    self.options.minWidth, Math.min(documentWidth, documentWidth - offset.left)
                );
                self.options.height = Ox.limit(
                    elementHeight - y + event.clientY,
                    self.options.minHeight, documentHeight - offset.top
                );
                that.width(self.options.width);
                that.height(self.options.height);
                that.$content.height(self.options.height - 48 - 2 * self.options.padding); // fixme: this should happen automatically
                triggerResizeEvent();
            });
            $window.one('mouseup', function() {
                $window.unbind('mousemove');
            });
        }

        function triggerResizeEvent() {
            that.triggerEvent('resize', {
                width: self.options.width,
                height: self.options.height
            });
        }

        self.onChange = function(key, value) {
            if (key == 'buttons') {
                loadButtons();
                /*
                that.$buttonsbar.children().animate({
                    opacity: 0
                }, 100, function() {
                    loadButtons();
                    that.$buttonsbar.children().animate({
                        opacity: 1
                    }, 100);
                });
                */
            } else if (key == 'content') {
                that.$content.html(value);
            } else if (key == 'height' || key == 'width') {
                that.animate({
                    height: self.options.height + 'px',
                    width: self.options.width + 'px'
                }, 100);
                that.$content.height(self.options.height - 48 - 2 * self.options.padding); // fixme: this should happen automatically
            } else if (key == 'title') {
                that.$title.animate({
                    opacity: 0
                }, 100, function() {
                    that.$title.html(value).animate({
                        opacity: 1
                    }, 100);
                });
            }
        }

        that.center = function() {
            
        };

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

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

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

        that.disableButton = function(id) {
            getButtonById(id).options({
                disabled: true
            });
            return that;
        };

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

        that.enableButton = function(id) {
            getButtonById(id).options({
                disabled: false
            });
            return that;
        };

        that.open = function() {
            Ox.print('before open')
            that.$layer.appendTo($body);
            that.css({
                opacity: 0
            }).appendTo($body).animate({
                opacity: 1
            }, 200);
            center();
            reset();
            // fixme: the following line prevents preview-style dialog
            that.gainFocus();
            $window.bind('mouseup', mouseupLayer)
            Ox.print('after open')
            return that;
        };

        that.size = function(width, height, callback) {
            $.extend(self, {
                initialWidth: width,
                initialHeight: height
            });
            $.extend(self.options, {
                width: width,
                height: height
            });
            // fixme: duplicated
            that.animate({
                height: self.options.height + 'px',
                width: self.options.width + 'px'
            }, 100, function() {
                that.$content.height(self.options.height - 48 - 2 * self.options.padding); // fixme: this should happen automatically
                callback();
            });
        }

        return that;

    }

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

    /**
    */
    Ox.Filter = function(options, self) {

        var self = self || {}
            that = new Ox.Element()
                .defaults({
                    
                })
                .options(options || {});

        return that;

    };

    /**
    */
    Ox.Form = function(options, self) {
        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    error: '',
                    id: '',
                    items: [],
                    submit: null
                })
                .options(options || {}) // fixme: the || {} can be done once, in the options function
                .addClass('OxForm');

        $.extend(self, {
            $items: [],
            $messages: [],
            formIsValid: false,
            itemIds: [],
            itemIsValid: []
        });

        // fixme: form isn't necessarily empty/invalid
        $.each(self.options.items, function(i, item) {
            self.itemIds[i] = item.options('id') || item.id;
            self.itemIsValid[i] = !!item.value().length;
            that.append(self.$items[i] = new Ox.FormItem({element: item}))
                .append(self.$messages[i] = new Ox.Element().addClass('OxFormMessage'));
            item.bindEvent({
                /*
            	blur: function(event, data) {
                	validate(i, data.valid);
                	if (data.valid) {
                    	self.$messages[i].html('').hide();
                	} else {
                    	self.$messages[i].html(data.message).show();
                	}
            	},
            	*/
            	autovalidate: function(event, data) {
            	    data.valid = !!data.value.length;
            	    validate(i, data.valid);
                	if (data.valid) {
                    	self.$messages[i].html('').hide();
                	} else {
                    	//self.$messages[i].html(data.message).show();
                    }
            	},
				submit: function(event, data) {
                	self.formIsValid && that.submit();
            	},
            	validate: function(event, data) {
                	validate(i, data.valid);
                	if (data.valid) {
                    	self.$messages[i].html('').hide();
                	} else {
                    	self.$messages[i].html(data.message).show();
                    }
            	}
			});
        });

        function getItemPositionById(id) {
            return self.itemIds.indexOf(id);
        }

        function setMessage(id, message) {
            self.$messages[getItemPositionById(id)].html(message)[message !== '' ? 'show' : 'hide']();
        }

        function submitCallback(data) {
            $.each(data, function(i, v) {
                setMessage(v.id, v.message);
            });
        }

        function validate(pos, valid) {
            Ox.print('FORM validate', pos, valid)
            self.itemIsValid[pos] = valid;
            if (Ox.every(self.itemIsValid) != self.formIsValid) {
                self.formIsValid = !self.formIsValid;
                that.triggerEvent('validate', {
                    valid: self.formIsValid
                });
            }
        }

        that.submit = function() {
            Ox.print('---- that.values()', that.values())
            self.options.submit(that.values(), submitCallback);
        };

        that.values = function() { // fixme: can this be private?
            /*
                get/set form values
                call without arguments to get current form values
                pass values as array to set values (not implemented)
            */
            var values = {};
            if (arguments.length == 0) {
                $.each(self.$items, function(i, $item) {
                    values[self.itemIds[i]] = self.$items[i].value();
                });
                Ox.print('VALUES', values)
                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('div', self)
                .defaults({
                    element: null,
                    error: '',
                })
                .options(options || {})
                .addClass('OxFormItem')
                .append(self.options.element);

        that.value = function() {
            return self.options.element.value();
        };

        return that;

    }

    /**
    Form Elements
    */

    /**
        methods:
            toggleDisabled              enable/disable button
            toggleSelected              select/unselect button
            toggleTitle                 if more than one title was provided,
                                        toggle to next title.
        events:
            click           non-selectable button was clicked
            deselect        selectable button was deselected
            select          selectable button was selected
    */
    Ox.Button = function(options, self) {
        var self = self || {},
            that = new Ox.Element('input', self)
                .defaults({
                    disabled: false,
                    group: false,
                    id: '',
                    overlap: 'none',
                    selectable: false,
                    selected: false,
                    size: 'medium',
                    // fixme: 'default' or ''?
                    style: 'default', // can be default, checkbox, symbol, or tab
                    title: '',
                    tooltip: '',
                    type: 'text',
                    width: 'auto'
                })
                .options(options || {})
                .attr({
                    disabled: self.options.disabled ? 'disabled' : '',
                    type: self.options.type == 'text' ? 'button' : 'image'
                })
                .addClass('OxButton Ox' + Ox.toTitleCase(self.options.size) +
                    (self.options.disabled ? ' OxDisabled': '') +
                    (self.options.selected ? ' OxSelected': '') +
                    (self.options.style != 'default' ? ' Ox' + Ox.toTitleCase(self.options.style) : '') +
                    (self.options.overlap != 'none' ? ' OxOverlap' + Ox.toTitleCase(self.options.overlap) : ''))
                .css(self.options.width == 'auto' ? {} : {
                    width: (self.options.width - 14) + 'px'
                })
                .mousedown(mousedown)
                .click(click);

        $.extend(self, Ox.isArray(self.options.title) ? {
            selectedTitle: Ox.setPropertyOnce(self.options.title, 'selected'),
            titles: self.options.title
        } : {
            selectedTitle: 0,
            titles: [{
                id: '',
                title: self.options.title
            }]
        });

        setTitle(self.titles[self.selectedTitle].title);

        if (self.options.tooltip) {
            self.tooltips = Ox.isArray(self.options.tooltip) ? self.options.tooltip : [self.options.tooltip];
            self.$tooltip = new Ox.Tooltip({
                    title: self.tooltips[self.selectedTitle]
                });
            that.mouseenter(mouseenter)
                .mouseleave(mouseleave);
        }

        function click() {
            if (!self.options.disabled) {
                var data = self.titles[self.selectedTitle];
                if (!self.options.selectable) {
                    that.triggerEvent('click', data);
                } else {
                    if (self.options.group) {
                        that.triggerEvent('select', data);
                    } else {
                        that.toggleSelected();
                    }
                }
                if (self.titles.length == 2) {
                    that.toggleTitle();
                }
            }
        }

        function mousedown(event) {
            if (self.options.type == 'image' && $.browser.safari) {
                // keep image from being draggable
                event.preventDefault();
            }
        }

        function mouseenter(event) {
            self.$tooltip.show(event.clientX, event.clientY);
        }

        function mouseleave(event) {
            self.$tooltip.hide();
        }

        function setTitle(title) {
            self.title = title;
            if (self.options.type == 'image') {
                that.attr({
                    src: oxui.path + 'png/ox.ui.' + Ox.theme() +
                        '/symbol' + Ox.toTitleCase(title) + '.png'
                });
            } else {
                that.val(title);
            }
        }

        self.onChange = function(key, value) {
            if (key == 'disabled') {
                that.attr({
                        disabled: value ? 'disabled' : ''
                    })
                    .toggleClass('OxDisabled');
            } else if (key == 'selected') {
                if (value != that.hasClass('OxSelected')) { // fixme: neccessary?
                    that.toggleClass('OxSelected');
                }
                that.triggerEvent('change');
            } else if (key == 'title') {
                setTitle(value);
            } else if (key == 'width') {
                that.$element.css({
                    width: (value - 14) + 'px'
                });
            }
        }

        that.toggleDisabled = function() {
            that.options({
                enabled: !self.options.disabled
            });
        }

        that.toggleSelected = function() {
            that.options({
                selected: !self.options.selected
            });
        }

        that.toggleTitle = function() {
            self.selectedTitle = 1 - self.selectedTitle;
            setTitle(self.titles[self.selectedTitle].title);
            self.$tooltip && self.$tooltip.options({
                title: self.tooltips[self.selectedTitle]
            });
        }

        return that;

    };

    /**
        options
            buttons     array of buttons
            max         integer, maximum number of selected buttons, 0 for all
            min         integer, minimum number of selected buttons, 0 for none
            selectable  if true, buttons are selectable
            type        string, 'image' or 'text'
        methods:
        events:
            change  {id, value}     selection within a group changed
    */
    Ox.ButtonGroup = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    buttons: [],
                    max: 1,
                    min: 1,
                    selectable: false,
                    size: 'medium',
                    style: '',
                    type: 'text',
                })
                .options(options || {})
                .addClass('OxButtonGroup');

        if (self.options.selectable) {
            self.optionGroup = new Ox.OptionGroup(
                self.options.buttons,
                self.options.min,
                self.options.max,
                'selected'
            );
            self.options.buttons = self.optionGroup.init();
        }

        self.$buttons = [];
        $.each(self.options.buttons, function(position, button) {
            var id = self.options.id + Ox.toTitleCase(button.id)
            self.$buttons[position] = Ox.Button({
                    disabled: button.disabled,
                    group: true,
                    id: id,
                    selectable: self.options.selectable,
                    selected: button.selected,
                    size: self.options.size,
                    style: self.options.style,
                    title: button.title,
                    type: self.options.type
                })
                .bindEvent('select', function() {
                    selectButton(position);
                })
                .appendTo(that);
        });

        function selectButton(pos) {
            var toggled = self.optionGroup.toggle(pos);
            if (toggled.length) {
                $.each(toggled, function(i, pos) {
                    self.$buttons[pos].toggleSelected();
                });
                that.triggerEvent('change', {
                    selected: $.map(self.optionGroup.selected(), function(v, i) {
                        return self.options.buttons[v].id;
                    })
                });
            }
        }

        return that;
    };

    /**
        options
            disabled        boolean, if true, checkbox is disabled
            id              element id
            group           boolean, if true, checkbox is part of a group
            checked         boolean, if true, checkbox is checked
            title           string, text on label
            width           integer, width in px
        methods:
            toggleChecked   function()
                            toggles checked property
                            returns that
        events:
            change          triggered when checked property changes
                            passes {checked, id, title}
    */
    Ox.Checkbox = function(options, self) {


        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    disabled: false,
                    id: '',
                    group: false,
                    checked: false,
                    title: '',
                    width: 'auto'
                })
                .options(options || {})
                .addClass('OxCheckbox')
                .attr(self.options.disabled ? {
                    disabled: 'disabled'
                } : {});

        if (self.options.title) {
            self.options.width != 'auto' && that.css({
                    width: self.options.width + 'px'
                });
            self.$title = new Ox.Label({
                    disabled: self.options.disabled,
                    id: self.options.id + 'Label',
                    overlap: 'left',
                    title: self.options.title,
                    width: self.options.width - 16
                })
                .css({
                    float: 'right'
                })
                .click(clickTitle)
                .appendTo(that);
        }

        self.$button = new Ox.Button({
                disabled: self.options.disabled,
                id: self.options.id + 'Button',
                title: [
                    {id: 'none', title: 'none', selected: !self.options.checked},
                    {id: 'check', title: 'check', selected: self.options.checked}
                ],
                type: 'image'
            })
            .addClass('OxCheckbox')
            .click(clickButton)
            .appendTo(that);

        function clickButton() {
            self.options.checked = !self.options.checked;
            // click will have toggled the button,
            // if it is part of a group, we have to revert that
            self.options.group && that.toggleChecked();
            that.triggerEvent('change', {
                checked: self.options.checked,
                id: self.options.id,
                title: self.options.title
            });
        }

        function clickTitle() {
            !self.options.disabled && self.$button.trigger('click');
        }

        self.onChange = function(key, value) {
            if (key == 'checked') {
                that.toggleChecked();
            }
        };

        that.checked = function() {
            return self.options.checked;
        }

        that.toggleChecked = function() {
            self.$button.toggleTitle();
            return that;
        }

        return that;

    };

    /**
        options
            checkboxes      []  array of checkboxes
            max             1   integer
            min             1   integer
            width           integer, width in px
        events:
            change          triggered when checked property changes
                            passes {checked, id, title}
    */
    Ox.CheckboxGroup = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    checkboxes: [],
                    max: 1,
                    min: 1,
                    width: 256
                })
                .options(options || {})
                .addClass('OxCheckboxGroup');

        self.optionGroup = new Ox.OptionGroup(
            self.options.checkboxes,
            self.options.min,
            self.options.max);
        self.options.checkboxes = self.optionGroup.init();

        $.extend(self, {
            $checkboxes: [],
            checkboxWidth: $.map(Ox.divideInt(
                self.options.width + (self.options.checkboxes.length - 1) * 6,
                self.options.checkboxes.length
            ), function(v, i) {
                return v + (i < self.options.checkboxes.length - 1 ? 10 : 0);
            })
        });
        $.each(self.options.checkboxes, function(position, checkbox) {
            var id = self.options.id + Ox.toTitleCase(checkbox.id)
            self.$checkboxes[position] = new Ox.Checkbox($.extend(checkbox, {
                    group: true,
                    id: id,
                    width: self.checkboxWidth[position]
                }))
                .bindEvent('change', function() {
                    change(position);
                })
                .appendTo(that);
        });

        function change(pos) {
            var toggled = self.optionGroup.toggle(pos);
            //Ox.print('change', pos, 'toggled', toggled)
            if (toggled.length) {
                $.each(toggled, function(i, pos) {
                    self.$checkboxes[pos].toggleChecked();
                });
                that.triggerEvent('change', {
                    checked: $.map(self.optionGroup.checked(), function(v, i) {
                        return self.options.checkboxes[v].id;
                    })
                });
            }
        }

        return that;

    };

    /**
        options
            arrows                      boolearn, if true, and type is 'float' or 'integer', display arrows
            arrowStep                   number, step when clicking arrows
            autocomplete                array of possible values, or
                                        function(key, value, callback), returns one or more values
            autocompleteReplace         boolean, if true, value is replaced
            autocompleteReplaceCorrect  boolean, if true, only valid values can be entered
            autocompleteSelect          boolean, if true, menu is displayed
            autocompleteSelectHighlight boolean, if true, value in menu is highlighted
            autocompleteSelectSubmit    boolean, if true, submit input on menu selection
            autocorrect                 string ('email', 'float', 'integer', 'phone', 'url'), or
                                        regexp(value), or
                                        function(key, value, blur, callback), returns value
            auto validate               --remote validation--
            clear                       boolean, if true, has clear button
            disabled                    boolean, if true, is disabled
            height                      integer, px (for type='textarea' and type='range' with orientation='horizontal')
            id                          string, element id
            key                         string, to be passed to autocomplete and autovalidate functions
            max                         number, max value if type is 'integer' or 'float'
            min                         number, min value if type is 'integer' or 'float'
            name                        string, will be displayed by autovalidate function ('invalid ' + name)
            overlap                     string, '', 'left' or 'right', will cause padding and negative margin
            picker
            //rangeOptions
                arrows              boolean, if true, display arrows
                //arrowStep           number, step when clicking arrows
                //arrowSymbols        array of two strings
                max                 number, maximum value
                min                 number, minimum value
                orientation         'horizontal' or 'vertical'
                step                number, step
                thumbValue          boolean, if true, value is displayed on thumb, or
                                    array of strings per value, or
                                    function(value), returns string
                thumbSize           integer, px
                trackGradient       string, css gradient for track
                trackImage          string, image url, or
                                    array of image urls
                //trackStep           number, 0 for 'scroll here', positive for step
                trackValues         boolean
            serialize
            textAlign                   'left', 'center' or 'right'
            type                        'float', 'integer', 'password', 'text'
            value                       string
            validate                    function, remote validation
            width                       integer, px
        methods:
        events:
            change
            submit
    */
    Ox.Input = function(options, self) {
        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    arrows: false,
                    arrowStep: 1,
                    autocomplete: null,
                    autocompleteReplace: false,
                    autocompleteReplaceCorrect: false,
                    autocompleteSelect: false,
                    autocompleteSelectHighlight: false,
                    autocompleteSelectSubmit: false,
                    autovalidate: null,
                    clear: false,
					disabled: false,
                    key: '',
                    min: 0,
                    max: 100,
                    label: '',
                    labelWidth: 64,
                    overlap: 'none',
                    placeholder: '',
                    serialize: null,
                    style: 'rounded',
                    textAlign: 'left',
                    type: 'text',
                    validate: null,
                    value: '',
                    width: 128
                })
                .options(options)
                .addClass('OxInput OxMedium Ox' + Ox.toTitleCase(self.options.style))
                .bindEvent($.extend(self.options.type == 'textarea' ? {} : {
                    key_enter: submit
                }, {
                    key_escape: cancel
                }));

        if (
            Ox.isArray(self.options.autocomplete) &&
            self.options.autocompleteReplace &&
            self.options.autocompleteReplaceCorrect &&
            self.options.value === ''
        ) {
            self.options.value = self.options.autocomplete[0]
        }

        // fixme: set to min, not 0
        if (self.options.type == 'float') {
            $.extend(self.options, {
                autovalidate: 'float',
                textAlign: 'right',
                value: self.options.value || '0.0'
            });
        } else if (self.options.type == 'integer') {
            $.extend(self.options, {
                autovalidate: 'integer',
                textAlign: 'right',
                value: self.options.value || '0'
            });
        }

        if (self.options.label) {
            self.$label = new Ox.Label({
                overlap: 'right',
                textAlign: 'right',
                title: self.options.label,
                width: self.options.labelWidth
            })
            .css({
                float: 'left', // fixme: use css rule
            })
            .click(function() {
                that.focus();
            })
            .appendTo(that);
        }

        if (self.options.arrows) {
            self.arrows = [];
            self.arrows[0] = [
                new Ox.Button({
                        overlap: 'right',
                        title: 'previous',
                        type: 'image'
                    })
                    .css({
                        float: 'left'
                    })
                    .click(function() {
                        clickArrow(0);
                    })
                    .appendTo(that),
                new Ox.Button({
                        overlap: 'left',
                        title: 'next',
                        type: 'image'
                    })
                    .css({
                        float: 'right'
                    })
                    .click(function() {
                        clickArrow(1);
                    })
                    .appendTo(that)
            ]
        }

        $.extend(self, {
            bindKeyboard: self.options.autocomplete || self.options.autovalidate,
            hasPasswordPlaceholder: self.options.type == 'password' && self.options.placeholder,
            inputWidth: getInputWidth()
        });

        if (self.options.clear) {
            self.$button = new Ox.Button({
                    overlap: 'left',
                    title: 'clear',
                    type: 'image'
                })
                .css({
                    float: 'right' // fixme: use css rule
                })
                .click(clear)
                .appendTo(that);
        }

        self.$input = $('<input>')
            .addClass('OxInput OxMedium Ox' + Ox.toTitleCase(self.options.style))
            .attr({
                disabled: self.options.disabled ? 'disabled' : '',
                type: self.options.type == 'password' ? 'password' : 'text'
            })
            .css({
                width: self.inputWidth + 'px',
                textAlign: self.options.textAlign
            })
            .val(self.options.value)
            .blur(blur)
            .change(change)
            .focus(focus)
            .appendTo(that.$element);

        if (self.hasPasswordPlaceholder) {
            self.$input.hide();
            self.$placeholder = $('<input>')
                .addClass('OxInput OxMedium Ox' +
                    Ox.toTitleCase(self.options.style) +
                    ' OxPlaceholder')
                .attr({
                    type: 'text'
                })
                .css({
                    //float: 'left',
                    width: self.inputWidth + 'px'
                })
                .val(self.options.placeholder)
                .focus(focus)
                .appendTo(that.$element);
        }

        if (self.options.autocomplete && self.options.autocompleteSelect) {
            self.$autocompleteMenu = constructAutocompleteMenu();
        }

        self.options.placeholder && setPlaceholder();

        function autocomplete(oldValue, oldCursor) {

            if (self.options.value || self.options.autocompleteReplaceCorrect) {
                Ox.isFunction(self.options.autocomplete) ?
                    (self.options.key ? self.options.autocomplete(
                        self.options.key,
                        self.options.value,
                        autocompleteCallback
                    ) : self.options.autocomplete(
                        self.options.value,
                        autocompleteCallback
                    )) : autocompleteCallback(autocompleteFunction(self.options.value));
            }
            if (!self.options.value) {
                self.options.autocompleteSelect && self.$autocompleteMenu.hideMenu();
            }

            function autocompleteFunction() {
                var values = Ox.find(self.options.autocomplete, self.options.value);
                return self.options.autocompleteReplace ? values[0] :
                        $.merge(values[0], values[1]);
            }

            function autocompleteCallback(values) {

                Ox.print('autocompleteCallback', values[0], self.options.value, self.options.value.length, oldValue, oldCursor)

                var length = self.options.value.length,
                    deleted = length <= oldValue.length - (oldCursor[1] - oldCursor[0]),
                    newValue = values[0] ?
                            ((self.options.autocompleteReplaceCorrect || !deleted) ?
                            values[0] : self.options.value) :
                            (self.options.autocompleteReplaceCorrect ? oldValue : self.options.value),
                    newLength = newValue.length,
                    pos = cursor(),
                    selected = -1,
                    selectEnd = length == 0 || (values[0] && values[0].length),
                    value;

                Ox.print('selectEnd', selectEnd)

                if (self.options.autocompleteReplace) {
                    self.options.value = newValue;
                    self.$input.val(self.options.value);
                    if (selectEnd) {
                        cursor(length, newLength);
                    } else if (self.options.autocompleteReplaceCorrect) {
                        cursor(oldCursor);
                    } else {
                        cursor(pos);
                    }
                    selected = 0;
                }

                if (self.options.autocompleteSelect) {
                    value = self.options.value.toLowerCase();
                    if (values.length) {
                        self.oldCursor = cursor();
                        self.oldValue = self.options.value;
                        self.$autocompleteMenu.options({
                            items: $.map(values, function(v, i) {
                                if (value == v.toLowerCase()) {
                                    selected = i;
                                }
                                return {
                                    id: v.toLowerCase().replace(/ /g, '_'), // fixme: need function to do lowercase, underscores etc?
                                    title: self.options.autocompleteSelectHighlight ? v.replace(
                                        new RegExp('(' + value + ')', 'ig'),
                                        '<span class="OxHighlight">$1</span>'
                                    ) : v
                                };
                            }),
                            selected: selected
                        }).showMenu();
                    } else {
                        self.$autocompleteMenu.hideMenu();
                    }
                }

                that.triggerEvent('autocomplete', {
                    value: newValue
                });

            }

        }

        function constructAutocompleteMenu() {
            var menu = new Ox.Menu({
                    element: self.$input,
                    id: self.options.id + 'Menu', // fixme: we do this in other places ... are we doing it the same way? var name?,
                    offset: {
                        left: 4,
                        top: 0
                    },
                    size: self.options.size
                })
                .bindEvent('click', clickMenu);
            if (self.options.autocompleteReplace) {
                menu.bindEvent({
                    deselect: deselectMenu,
                    select: selectMenu,
                });
            }
            return menu;
        }

        function autovalidate() {

            var blur, oldCursor, oldValue;

            if (arguments.length == 1) {
                blur = arguments[0];
            } else {
                blur = false;
                oldValue = arguments[0];
                oldCursor = arguments[1];
            }

            Ox.isFunction(self.options.autovalidate) ?
                (self.options.key ? self.options.autovalidate(
                    self.options.key,
                    self.options.value,
                    blur,
                    autovalidateCallback
                ) : self.options.autovalidate(
                    self.options.value,
                    blur,
                    autovalidateCallback
                )) : (Ox.isRegExp(self.options.autovalidate) ?
                    autovalidateCallback(autovalidateFunction(self.options.value)) :
                    autovalidateTypeFunction(self.options.type, self.options.value)
                );

            function autovalidateFunction(value) {
                var regexp = new RegExp(self.options.autovalidate);
                return $.map(value.split(''), function(v) {
                    return regexp(v) ? v : null;
                }).join('');
            }

            function autovalidateTypeFunction(type, value) {
                // fixme: remove trailing zeroes on blur
                var cursor,
                    regexp = type == 'float' ? /[\d\.]/ : /\d/;
                if (type == 'float') {
                    if (value.indexOf('.') != value.lastIndexOf('.')) {
                        value = oldValue;
                    } else {
                        if (self.autovalidateFloatFlag) {
                            if (Ox.endsWith(value, '.')) {
                                value = value.substr(0, value.length - 1);
                            }
                            self.autovalidateFloatFlag = false;
                        }
                        while (value[0] == '0' && value[1] != '.') {
                            value = value.substr(1);
                        }
                        while (Ox.startsWith(value, '.')) {
                            if (Ox.startsWith(value, '..')) {
                                value = value.substr(1);
                            } else {
                                value = '0' + value;
                            }
                        }
                        if (Ox.endsWith(value, '.')) {
                            value += '0';
                            cursor = [value.length - 1, value.length];
                            self.autovalidateFloatFlag = true;
                        }
                    }
                }
                value = $.map(value.split(''), function(v) {
                    return regexp(v) ? v : null;
                }).join('');
                if (type == 'integer') {
                    while (value.length > 1 && Ox.startsWith(value, '0')) {
                        value = value.substr(1);
                    }
                }
                if (value === '') {
                    value = type == 'float' ? '0.0' : '0';
                    cursor = [0, value.length];
                } else if (value > self.options.max) {
                    value = oldValue;
                }
                autovalidateCallback(value, cursor);
            }

            function autovalidateCallback(newValue, newCursor) {
                Ox.print('autovalidateCallback', newValue, oldCursor)
                self.options.value = newValue;
                self.$input.val(self.options.value);
                !blur && cursor(
                    newCursor || (oldCursor[1] + newValue.length - oldValue.length)
                );
                that.triggerEvent('autovalidate', {
                    value: self.options.value
                });
            }

        }

        /*

        function autovalidate(blur) {
            Ox.print('autovalidate', self.options.value, blur || false)
            self.autocorrectBlur = blur || false;
            self.autocorrectCursor = cursor();
            Ox.isFunction(self.options.autocorrect) ?
                (self.options.key ? self.options.autocorrect(
                    self.options.key,
                    self.options.value,
                    self.autocorrectBlur,
                    autocorrectCallback
                ) : self.options.autocorrect(
                    self.options.value,
                    self.autocorrectBlur,
                    autocorrectCallback
                )) : autocorrectCallback(autocorrect(self.options.value));
        }

        function autovalidateFunction(value) {
            var length = value.length;
            return $.map(value.toLowerCase().split(''), function(v, i) {
                if (new RegExp(self.options.autocorrect)(v)) {
                    return v;
                } else {
                    return null;
                }
            }).join('');
        }

        */

        function blur() {
            Ox.print('blur')
            that.loseFocus();
            //that.removeClass('OxFocus');
            self.options.value = self.$input.val();
            self.options.autovalidate && autovalidate(true);
            self.options.placeholder && setPlaceholder();
            self.options.validate && validate();
            if (self.bindKeyboard) {
                $document.unbind('keydown', keypress);
                $document.unbind('keypress', keypress);
            }
            that.triggerEvent('blur', {});
        }

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

        function change() {
            self.options.value = self.$input.val();
            that.triggerEvent('change', {
                value: self.options.value
            });
        }

        function clear() {
            // fixme: set to min, not zero
            // fixme: make this work for password
            var value = '';
            if (self.options.type == 'float') {
                value = '0.0';
            } else if (self.options.type == 'integer') {
                value = '0'
            }
            self.$input.val(value);
            cursor(0, value.length);
        }

        function clickArrow(i) {
            self.options.value = Ox.limit(
                parseFloat(self.options.value) + (i == 0 ? -1 : 1) * self.options.arrowStep,
                self.options.min,
                self.options.max
            ).toString();
            self.$input.val(self.options.value);//.focus();
        }

        function clickMenu(event, data) {
            Ox.print('clickMenu', data);
            self.options.value = data.title;
            self.$input.val(self.options.value).focus();
            that.gainFocus();
            self.options.autocompleteSelectSubmit && submit();
        }

        function cursor(start, end) {
            /*
            cursor()                returns [start, end]
            cursor(start)           sets start
            cursor([start, end])    sets start and end
            cursor(start, end)      sets start and end
            */
            var isArray = Ox.isArray(start);
            if (arguments.length == 0) {
                return [self.$input[0].selectionStart, self.$input[0].selectionEnd];
            } else {
                end = isArray ? start[1] : (end ? end : start);
                start = isArray ? start[0] : start;
                self.$input[0].setSelectionRange(start, end);
            }
        }

        function deselectMenu() {
            self.options.value = self.oldValue;
            self.$input.val(self.options.value);
            cursor(self.oldCursor);
        }

        function focus() {
            Ox.print('focus()')
            if (
                that.hasClass('OxFocus') || // fixme: this is just a workaround, since for some reason, focus() gets called twice on focus
                (self.$autocompleteMenu && self.$autocompleteMenu.is(':visible')) ||
                (self.hasPasswordPlaceholder && self.$input.is(':visible'))
            ) {
                return;
            }
            that.gainFocus();
            self.options.placeholder && setPlaceholder();
            if (self.bindKeyboard) {
                Ox.print('binding...')
                // fixme: different in webkit and firefox (?), see keyboard handler, need generic function
                $document.keydown(keypress);
                $document.keypress(keypress);
                self.options.autocompleteSelect && setTimeout(autocomplete, 0); // fixme: why is the timeout needed?
            }
        }

        function getInputWidth() {
            return self.options.width -
                (self.options.arrows ? 32 : 0) -
                (self.options.clear ? 16 : 0) -
                (self.options.label ? self.options.labelWidth : 0) -
                (self.options.style == 'rounded' ? 14 : 6);
        }

        function keypress(event) {
            var oldCursor = cursor(),
                oldValue = self.options.value,
                newValue = oldValue.substr(0, oldCursor[0] - 1),
                hasDeletedSelectedEnd = (event.keyCode == 8 || event.keyCode == 46) &&
                    oldCursor[0] < oldCursor[1] && oldCursor[1] == oldValue.length;
            Ox.print('keypress', event.keyCode)
            if (event.keyCode != 9 && event.keyCode != 13 && event.keyCode != 27) { // fixme: can't 13 and 27 return false?
                setTimeout(function() { // wait for val to be set
                    var value = self.$input.val();
                    if (self.options.autocompleteReplaceCorrect && hasDeletedSelectedEnd) {
                        Ox.print(value, '->', newValue);
                        value = newValue; // value.substr(0, value.length - 1);
                        self.$input.val(value);
                    }
                    if (value != self.options.value) {
                        self.options.value = value;
                        self.options.autocomplete && autocomplete(oldValue, oldCursor);
                        self.options.autovalidate && autovalidate(oldValue, oldCursor);
                    }
                }, 0);
            }
            if ((event.keyCode == 38 || event.keyCode == 40) && self.options.autocompleteSelect && self.$autocompleteMenu.is(':visible')) {
                return false;
            }
        }

        function selectMenu(event, data) {
            var pos = cursor();
            Ox.print('selectMenu', pos)
            self.options.value = data.title
            self.$input.val(self.options.value);
            cursor(pos[0], self.options.value.length)
        }

        function setPlaceholder() {
            if (self.options.placeholder) {
                if (that.hasClass('OxFocus')) {
                    if (self.options.value === '') {
                        if (self.options.type == 'password') {
                            self.$placeholder.hide();
                            self.$input.show().focus();
                        } else {
                            self.$input
                                .removeClass('OxPlaceholder')
                                .val('');
                        }
                    }
                } else {
                    if (self.options.value === '') {
                        if (self.options.type == 'password') {
                            self.$input.hide();
                            self.$placeholder.show();
                        } else {
                            self.$input
                                .addClass('OxPlaceholder')
                                .val(self.options.placeholder)
                        }
                    } else {
                        self.$input
                            .removeClass('OxPlaceholder')
                            .val(self.options.value)
                    }
                }
            }
        }

        function setWidth() {
            
        }

        function submit() {
            self.$input.blur();
            that.triggerEvent('submit', {
                value: self.options.value
            });
        }

        function validate() {
            self.options.validate(self.options.value, function(data) {
                that.triggerEvent('validate', data);
            });
        }

        self.onChange = function(key, value) {
            var inputWidth, val;
            if (['autocomplete', 'autocompleteReplace', 'autocompleteSelect', 'autovalidate'].indexOf(key) > -1) {
                if (self.options.autocomplete && self.options.autocompleteSelect) {
                    self.$autocompleteMenu = constructAutocompleteMenu();
                }
                self.bindKeyboard = self.options.autocomplete || self.options.autovalidate;
            } else if (key == 'disabled') {
				self.$input.attr({
					disabled: value ? 'disabled' : ''
				});
			} else if (key == 'placeholder') {
                setPlaceholder();
            } else if (key == 'value') {
                val = self.$input.val();
                self.$input.val(value);
                setPlaceholder();
            } else if (key == 'width') {
                inputWidth = getInputWidth();
                self.$input.css({
                    width: inputWidth + 'px'
                });
                self.hasPasswordPlaceholder && self.$placeholder.css({
                    width: inputWidth + 'px'
                });
            }
        };

        that.focus = function() {
            self.$input.focus();
            cursor(0, self.$input.val().length);
            return that;
        };

        that.value = function() {
            return self.$input.hasClass('OxPlaceholder') ? '' : self.$input.val();
        };

        return that;

    };

    Ox.AutocorrectIntFunction = function(min, max, pad, year) {
        var pad = pad || false,
            year = year || false,
            maxLength = max.toString().length,
            ret = null,
            values = [];
            $.each(Ox.range(min, max + 1), function(i, v) {
                values.push(v + '');
                pad && v.toString().length < maxLength && values.push(Ox.pad(v, maxLength));
            });
        return function(value, blur, callback) {
            var results;
            if (year && value == '1') {
                value = '1900';
            } else {
                results = Ox.find(values, value);
                value = results[0].length == 1 && results[0][0].length < maxLength ?
                    (pad ? Ox.pad(results[0][0], maxLength) : results[0][0]) :
                    (results[0].length ? results[0][0] : null);
            }
            callback(value);
        };
    };

    Ox.InputGroup = function(options, self) {

        /***
        Ox.InputGroup
        Options:
        Methods:
        Events:
        ***/

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    id: '',
                    inputs: [],
                    separators: [],
                    width: 0
                })
                .options(options || {})
                .addClass('OxInputGroup')
                .click(click);

        if (self.options.width) {
            setWidths();
        } else {
            self.options.width = getWidth();
        }
        that.css({
            width: self.options.width + 'px'
        });

        $.extend(self, {
            //$input: [],
            $separator: []
        });

        $.each(self.options.separators, function(i, v) {
            self.$separator[i] = new Ox.Label({
                textAlign: 'center',
                title: v.title,
                width: v.width + 32
            })
            .addClass('OxSeparator')
            .css({
                marginLeft: (self.options.inputs[i].options('width') - (i == 0 ? 16 : 32)) + 'px'
            })
            .appendTo(that);
        });

        $.each(self.options.inputs, function(i, $input) {
            $input.options({
                    id: self.options.id + Ox.toTitleCase($input.options('id')),
                    parent: that
                })
                .css({
                    marginLeft: -Ox.sum($.map(self.options.inputs, function(v_, i_) {
                        return i_ > i ? self.options.inputs[i_ - 1].options('width') +
                            self.options.separators[i_ - 1].width : (i_ == i ? 16 : 0);
                    })) + 'px'
                })
                .bindEvent({
                    change: change,
                    submit: change,
                    validate: validate
                })
                .appendTo(that);
        });

        function change(event, data) {
            Ox.print('InputGroup change')
            // fixme: would be good to pass a value here
            that.triggerEvent('change');
        }

        function click(event) {
            if ($(event.target).hasClass('OxSeparator')) {
                self.options.inputs[0].focus();
            }
        }

        function getWidth() {
            return Ox.sum($.map(self.options.inputs, function(v, i) {
                return v.options('width');
            })) + Ox.sum($.map(self.options.separators, function(v, i) {
                return v.width;
            }));
        }

        function setWidths() {
            var length = self.options.inputs.length,
                inputWidths = Ox.divideInt(
                    self.options.width - Ox.sum($.map(self.options.separators, function(v, i) {
                        return v.width;
                    })), length
                );
            $.each(self.options.inputs, function(i, v) {
                v.options({
                    width: inputWidths[1]
                });
            });
        }

        function validate(event, data) {
            Ox.print('INPUTGROUP TRIGGER VALIDATE')
            that.triggerEvent('validate', data);
        }

        // fixme: is this used?
        that.getInputById = function(id) {
            var input = null;
            $.each(self.options.inputs, function(i, v) {
                Ox.print(v, v.options('id'), id)
                if (v.options('id') == self.options.id + Ox.toTitleCase(id)) {
                    input = v;
                    return false;
                }
            });
            return input;
        };

        that.value = function() {
            return $.map(self.options.inputs, function(input) {
                var ret = null;
                ['checked', 'selected', 'value'].forEach(function(v) {
                    input[v] && (ret = input[v]());
                });
                return ret;
            });
        };

        return that;

    };

    Ox.ColorInput = function(options, self) {

        var self = $.extend(self || {}, {
                options: $.extend({
                    id: '',
                    value: '0, 0, 0'
                }, options)
            }),
            that;

        self.values = self.options.value.split(', ');
        self.$inputs = [];
        $.each(['red', 'green', 'blue'], function(i, v) {
            self.$inputs[i] = new Ox.Input({
                id: v,
                max: 255,
                type: 'integer',
                value: self.values[i],
                width: 36
            })
            .bindEvent('autovalidate', change);
        });
        self.$inputs[3] = new Ox.Label({
                id: 'color',
                width: 36
            })
            .css({
                background: 'rgb(' + self.options.value + ')'
            });
        self.$inputs[4] = new Ox.ColorPicker({
                id: 'picker'
            })
            .bindEvent('change', function(event, data) {
                Ox.print('change function called');
                self.options.value = data.value;
                self.values = data.value.split(', ');
                $.each(Ox.range(3), function(i) {
                    self.$inputs[i].options({
                        value: self.values[i]
                    });
                });
            })
            .options({
                width: 16 // this is just a hack to make the InputGroup layout work
            });

        that = new Ox.InputGroup({
                id: self.options.id,
                inputs: self.$inputs,
                separators: [
                    {title: ',', width: 8},
                    {title: ',', width: 8},
                    {title: '', width: 8},
                    {title: '', width: 8}
                ],
                value: self.options.value // fixme: it'd be nicer if this would be taken care of by passing self
            }, self)
            .bindEvent('change', change);

        function change() {
            self.options.value = $.map(self.$inputs, function(v, i) {
                return v.options('value');
            }).join(', ');
            self.$inputs[3].css({
                background: 'rgb(' + self.options.value + ')'
            });
        }

        return that;

    };

    /**
        options:
            format: 'short'
            value:  date value
            weekday: false
            width: {
                day: 32,
                month: options.format == 'long' ? 80 : (options.format == 'medium' ? 40 : 32),
                weekday: options.format == 'long' ? 80 : 40,
                year: 48
            }
    */
    Ox.DateInput = function(options, self) {

        var self = $.extend(self || {}, {
                options: $.extend({
                    format: 'short',
                    value: Ox.formatDate(new Date(), '%F'),
                    weekday: false,
                    width: {
                        day: 32,
                        month: options.format == 'long' ? 80 : (options.format == 'medium' ? 40 : 32),
                        weekday: options.format == 'long' ? 80 : 40,
                        year: 48
                    }
                }, options)
            }),
            that;

        $.extend(self, {
            date: new Date(self.options.value.replace(/-/g, '/')),
            formats: {
                day: '%d',
                month: self.options.format == 'short' ? '%m' :
                        (self.options.format == 'medium' ? '%b' : '%B'),
                weekday: self.options.format == 'long' ? '%A' : '%a',
                year: '%Y'
            },
            months: self.options.format == 'long' ? Ox.MONTHS : $.map(Ox.MONTHS, function(v, i) {
                return v.substr(0, 3);
            }),
            weekdays: self.options.format == 'long' ? Ox.WEEKDAYS : $.map(Ox.WEEKDAYS, function(v, i) {
                return v.substr(0, 3);
            })
        });

        self.$input = $.extend(self.options.weekday ? {
            weekday: new Ox.Input({
                    autocomplete: self.weekdays,
                    autocompleteReplace: true,
                    autocompleteReplaceCorrect: true,
                    id: 'weekday',
                    value: Ox.formatDate(self.date, self.formats.weekday),
                    width: self.options.width.weekday
                })
                .bindEvent('autocomplete', changeWeekday),
        } : {}, {
            day: new Ox.Input({
                    autocomplete: $.map(Ox.range(1, Ox.getDaysInMonth(
                        parseInt(Ox.formatDate(self.date, '%Y'), 10),
                        parseInt(Ox.formatDate(self.date, '%m'), 10)
                    ) + 1), function(v, i) {
                        return self.options.format == 'short' ? Ox.pad(v, 2) : v.toString();
                    }),
                    autocompleteReplace: true,
                    autocompleteReplaceCorrect: true,
                    id: 'day',
                    value: Ox.formatDate(self.date, self.formats.day),
                    textAlign: 'right',
                    width: self.options.width.day
                })
                .bindEvent('autocomplete', changeDay),
            month: new Ox.Input({
                    autocomplete: self.options.format == 'short' ? $.map(Ox.range(1, 13), function(v, i) {
                        return Ox.pad(v, 2);
                    }) : self.months,
                    autocompleteReplace: true,
                    autocompleteReplaceCorrect: true,
                    id: 'month',
                    value: Ox.formatDate(self.date, self.formats.month),
                    textAlign: self.options.format == 'short' ? 'right' : 'left',
                    width: self.options.width.month
                })
                .bindEvent('autocomplete', changeMonthOrYear),
            year: new Ox.Input({
                    autocomplete: $.map($.merge(Ox.range(1900, 3000), Ox.range(1000, 1900)), function(v, i) {
                        return v.toString();
                    }),
                    autocompleteReplace: true,
                    autocompleteReplaceCorrect: true,
                    id: 'year',
                    value: Ox.formatDate(self.date, self.formats.year),
                    textAlign: 'right',
                    width: self.options.width.year
                })
                .bindEvent('autocomplete', changeMonthOrYear)
        });

        that = new Ox.InputGroup($.extend(self.options, {
            id: self.options.id,
            inputs: $.merge(self.options.weekday ? [
                self.$input.weekday
            ] : [], self.options.format == 'short' ? [
                self.$input.year, self.$input.month, self.$input.day
            ] : [
                self.$input.month, self.$input.day, self.$input.year
            ]),
            separators: $.merge(self.options.weekday ? [
                {title: self.options.format == 'short' ? '' : ',', width: 8},
            ] : [], self.options.format == 'short' ? [
                {title: '-', width: 8}, {title: '-', width: 8}
            ] : [
                {title: '', width: 8}, {title: ',', width: 8}
            ]),
            width: 0
        }), self);

        Ox.print('SELF', self)

        function changeDay() {
            self.options.weekday && self.$input.weekday.options({
                value: Ox.formatDate(new Date([
                    self.$input.month.options('value'),
                    self.$input.day.options('value'),
                    self.$input.year.options('value')
                ].join(' ')), self.formats.weekday)
            });
            setValue();
        }

        function changeMonthOrYear() {
            var day = self.$input.day.options('value'),
                month = self.$input.month.options('value'),
                year = self.$input.year.options('value'),
                days = Ox.getDaysInMonth(year, self.options.format == 'short' ? parseInt(month, 10) : month);
            day = day <= days ? day : days;
            Ox.print(year, month, 'day days', day, days)
            self.options.weekday && self.$input.weekday.options({
                value: Ox.formatDate(new Date([month, day, year].join(' ')), self.formats.weekday)
            });
            self.$input.day.options({
                autocomplete: $.map(Ox.range(1, days + 1), function(v, i) {
                    return self.options.format == 'short' ? Ox.pad(v, 2) : v.toString();
                }),
                value: self.options.format == 'short' ? Ox.pad(day, 2) : day.toString()
            });
            setValue();
        }

        function changeWeekday() {
            var date = getDateInWeek(
                    self.$input.weekday.options('value'),
                    self.$input.month.options('value'),
                    self.$input.day.options('value'),
                    self.$input.year.options('value')
                );
            self.$input.month.options({value: date.month});
            self.$input.day.options({
                autocomplete: $.map(Ox.range(1, Ox.getDaysInMonth(date.year, date.month) + 1), function(v, i) {
                        return self.options.format == 'short' ? Ox.pad(v, 2) : v.toString();
                    }),
                value: date.day
            });
            self.$input.year.options({value: date.year});
            setValue();
        }

        function getDateInWeek(weekday, month, day, year) {
            Ox.print([month, day, year].join(' '))
            var date = new Date([month, day, year].join(' '));
            date = Ox.getDateInWeek(date, weekday);
            return {
                day: Ox.formatDate(date, self.formats.day),
                month: Ox.formatDate(date, self.formats.month),
                year: Ox.formatDate(date, self.formats.year)
            };
        }

        function setValue() {
            self.options.value = Ox.formatDate(new Date(self.options.format == 'short' ? [
                self.$input.year.options('value'),
                self.$input.month.options('value'),
                self.$input.day.options('value')
            ].join('/') : [
                self.$input.month.options('value'),
                self.$input.day.options('value'),
                self.$input.year.options('value')
            ].join(' ')), '%F');
        }

        /*
        function normalize() {
            var year = that.getInputById('year').options('value'),
                month = that.getInputById('month').options('value'),
                day = that.getInputById('day').options('value')
            return {
                year: year,
                month: self.options.format == 'short' ? month :
                        Ox.pad((format == 'medium' ? Ox.WEEKDAYS.map(function(v, i) {
                            return v.substr(0, 3);
                        }) : Ox.WEEKDAYS).indexOf(month), 2),
                day: Ox.pad(day, 2)
            }
        }
        */

        /*
        that.serialize = function() {
            var normal = normalize();
            return [normal.year, normal.month, normal.day].join('-');
        }
        */

        return that;

    };

    Ox.DateTimeInput = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    ampm: false,
                    format: 'short',
                    seconds: false,
                    value: Ox.formatDate(new Date(), '%F %T'),
                    weekday: false
                })
                .options(options || {});

        self.values = self.options.value.split(' ');
        Ox.print(self.values)

        that = new Ox.InputGroup({
                inputs: [
                    new Ox.DateInput({
                        format: self.options.format,
                        id: 'date',
                        value: self.values[0],
                        weekday: self.options.weekday
                    }),
                    new Ox.TimeInput({
                        ampm: self.options.ampm,
                        id: 'time',
                        value: self.values[1],
                        seconds: self.options.seconds
                    })
                ],
                separators: [
                    {title: '', width: 8}
                ],
                value: self.options.value
            })
            .bindEvent('change', setValue);

        function setValue() {
            self.options.value = [
                self.options('inputs')[0].options('value'),
                self.options('inputs')[1].options('value')
            ].join(' ');
        }

        return that;

    };

    Ox.PlaceInput = function(options, self) {

        var self = $.extend(self || {}, {
                options: $.extend({
                    id: '',
                    value: 'United States'
                }, options)
            }),
            that;

        that = new Ox.FormElementGroup({
                id: self.options.id,
                elements: [
                    new Ox.Input({
                        id: 'input',
                        value: self.options.value
                    }),
                    new Ox.PlacePicker({
                        id: 'picker',
                        overlap: 'left',
                        value: self.options.value
                    })
                ],
                float: 'right'
            }, self)
            .bindEvent('change', change);

        function change() {
            
        }

        return that;

    };

    Ox.TimeInput = function(options, self) {

        // fixme: seconds get set even if options.seconds is false
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    ampm: false,
                    seconds: false,
                    milliseconds: false,
                    value: Ox.formatDate(new Date(), '%T'),
                })
                .options(options || {});

        if (self.options.milliseconds) {
            self.options.seconds = true;
            if (self.options.value.indexOf('.') == -1) {
                self.options.value += '.000';
            }
        }

        self.date = getDate();
        self.values = getValues();

        self.$input = {
            hours: Ox.Input({
                autocomplete: $.map(self.options.ampm ? Ox.range(1, 13) : Ox.range(0, 24), function(v) {
                    return Ox.pad(v, 2);
                }),
                autocompleteReplace: true,
                autocompleteReplaceCorrect: true,
                id: 'hours',
                textAlign: 'right',
                value: self.values.hours,
                width: 32
            }),
            minutes: Ox.Input({
                autocomplete: $.map(Ox.range(0, 60), function(v) {
                    return Ox.pad(v, 2);
                }),
                autocompleteReplace: true,
                autocompleteReplaceCorrect: true,
                id: 'minutes',
                textAlign: 'right',
                value: self.values.minutes,
                width: 32
            }),
            seconds: Ox.Input({
                autocomplete: $.map(Ox.range(0, 60), function(v) {
                    return Ox.pad(v, 2);
                }),
                autocompleteReplace: true,
                autocompleteReplaceCorrect: true,
                id: 'seconds',
                textAlign: 'right',
                value: self.values.seconds,
                width: 32
            }),
            milliseconds: Ox.Input({
                autocomplete: $.map(Ox.range(0, 1000), function(v) {
                    return Ox.pad(v, 3);
                }),
                autocompleteReplace: true,
                autocompleteReplaceCorrect: true,
                id: 'milliseconds',
                textAlign: 'right',
                value: self.values.milliseconds,
                width: 40
            }),
            ampm: Ox.Input({
                autocomplete: ['AM', 'PM'],
                autocompleteReplace: true,
                autocompleteReplaceCorrect: true,
                id: 'ampm',
                value: self.values.ampm,
                width: 32
            })
        };

        that = new Ox.InputGroup($.extend(self.options, {
                inputs: $.merge($.merge($.merge([
                    self.$input.hours,
                    self.$input.minutes,
                ], self.options.seconds ? [
                    self.$input.seconds
                ] : []), self.options.milliseconds ? [
                    self.$input.milliseconds
                ] : []), self.options.ampm ? [
                    self.$input.ampm
                ] : []),
                separators: $.merge($.merge($.merge([
                    {title: ':', width: 8},
                ], self.options.seconds ? [
                    {title: ':', width: 8}
                ] : []), self.options.milliseconds ? [
                    {title: '.', width: 8}
                ] : []), self.options.ampm ? [
                    {title: '', width: 8}
                ] : []),
                //width: self.options.width || 128
            }), self)
            .bindEvent('change', setValue);

        setValue();

        function getDate() {
            return new Date('1970/01/01 ' + (
                self.options.milliseconds ?
                self.options.value.substr(0, self.options.value.length - 4) :
                self.options.value
            ));
        }

        function getValues() {
            self.date = getDate();
            return {
                ampm: Ox.formatDate(self.date, '%p'),
                hours: Ox.formatDate(self.date, self.options.ampm ? '%I' : '%H'),
                milliseconds: self.options.milliseconds ? self.options.value.substr(-3) : '000',
                minutes: Ox.formatDate(self.date, '%M'),
                seconds: Ox.formatDate(self.date, '%S')
            };
        }

        function setValue() {
            self.options.value = Ox.formatDate(new Date('1970/01/01 ' + [
                self.$input.hours.options('value'),
                self.$input.minutes.options('value'),
                self.options.seconds ? self.$input.seconds.options('value') : '00'
            ].join(':') + (self.options.ampm ? ' ' + self.$input.ampm.options('value') : '')),
                (self.options.seconds? '%T' : '%H:%M')) +
                (self.options.milliseconds ? '.' + self.$input.milliseconds.options('value') : '');
            Ox.print('SETVALUE', self.options.value);
        }

        function setValues() {
            self.values = getValues();
            $.each(self.$input, function(k, v) {
                self.$input[k].options({
                    value: self.values[k]
                });
            });
        }

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

        return that;

    };

    Ox.Label = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    disabled: false,
                    id: '',
                    overlap: 'none',
                    textAlign: 'left',
                    title: '',
                    width: 'auto'
                })
                .options(options)
                .addClass(
                    'OxLabel' + (self.options.disabled ? ' OxDisabled' : '') +
                    (self.options.overlap != 'none' ?
                    ' OxOverlap' + Ox.toTitleCase(self.options.overlap) : '')
                )
                .css($.extend(self.options.width == 'auto' ? {} : {
                    width: (self.options.width - 14) + 'px'
                }, {
                    textAlign: self.options.textAlign
                }))
                .html(self.options.title);

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

        return that;

    };

    Ox.OptionGroup = function(items, min, max, property) {

        /*
            to be used by ButtonGroup, CheckboxGroup, Select and Menu
        */

        var property = property || 'checked'
            length = items.length,
            max = max == -1 ? length : max;

        function getLastBefore(pos) {
            // returns the position of the last checked item before position pos
            var last = -1;
            Ox.print(items, items.length, length, $.merge(
                pos > 0 ? Ox.range(pos - 1, -1, -1) : [],
                pos < items.length - 1 ? Ox.range(items.length - 1, pos, -1) : []
            ))
            // fixme: why is length not == items.length here?
            $.each($.merge(
                pos > 0 ? Ox.range(pos - 1, -1, -1) : [],
                pos < items.length - 1 ? Ox.range(items.length - 1, pos, -1) : []
            ), function(i, v) {
                Ox.print(pos, v)
                if (items[v][property]) {
                    last = v;
                    return false;
                }
            });
            return last;
        }

        function getNumber() {
            // returns the number of checked items
            var num = 0;
            $.each(items, function(i, item) {
                if (item[property]) {
                    num++;
                }
            })
            return num;
        }

        this[property] = function() {
            // returns an array with the positions of all checked item
            var checked = [];
            $.each(items, function(i, item) {
                if (item[property]) {
                    checked.push(i);
                }
            })
            return checked;
        };

        this.init = function() {
            var num = getNumber(),
                count = 0;
            //if (num < min || num > max) {
                $.each(items, function(i, item) {
                    if (Ox.isUndefined(item[property])) {
                        item[property] = false;
                    }
                    if (item[property]) {
                        count++;
                        if (count > max) {
                            item[property] = false;
                        }
                    } else {
                        if (num < min) {
                            item[property] = true;
                            num++;
                        }
                    }
                });
            //}
            return items;
        };

        this.toggle = function(pos) {
            var last,
                num = getNumber(),
                toggled = [];
            if (!items[pos][property]) { // check
                if (num >= max) {
                    last = getLastBefore(pos);
                    items[last][property] = false;
                    toggled.push(last);
                }
                if (!items[pos][property]) {
                    items[pos][property] = true;
                    toggled.push(pos);
                }
            } else { // uncheck
                if (num > min) {
                    items[pos][property] = false;
                    toggled.push(pos);
                }
            }
            return toggled;
        }

        return this;

    }

    /**
        options
            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
            trackGradient   array           colors
            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
            valueNames      array           value names to display on thumb
    */
    Ox.Range = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    arrows: false,
                    arrowStep: 1,
                    arrowSymbols: ['previous', 'next'],
                    max: 100,
                    min: 0,
                    orientation: 'horizontal',
                    step: 1,
                    size: 128,
                    thumbSize: 16,
                    thumbValue: false,
                    trackColors: [],
                    trackImages: [],
                    trackStep: 0,
                    value: 0,
                    valueNames: null,
                })
                .options($.extend(options, {
                    arrowStep: options.arrowStep ?
                        options.arrowStep : options.step,
                    trackImages: $.makeArray(options.trackImages || [])
                }))
                .addClass('OxRange')
                .css({
                    width: self.options.size + 'px'
                });

        $.extend(self, {
            trackColors: self.options.trackColors.length,
            trackImages: self.options.trackImages.length,
            trackSize: self.options.size - self.options.arrows * 32,
            values: (self.options.max - self.options.min + self.options.step) /
                    self.options.step
        });
        $.extend(self, {
            thumbSize: Math.max(self.trackSize / self.values, self.options.thumbSize),
            trackImageWidths: self.trackImages == 1 ? [self.trackSize - 16] :
                    Ox.divideInt(self.trackSize - 2, self.trackImages)
        });
        $.extend(self, {
            trackColorsStart: self.thumbSize / 2 / self.options.size,
            trackColorsStep: (self.options.size - self.thumbSize) / (self.trackColors - 1) / self.options.size
        });

        if (self.options.arrows) {
            self.$arrows = [];
            $.each(Ox.range(0, 2), function(i) {
                self.$arrows[i] = Ox.Button({
                    overlap: i == 0 ? 'right' : 'left',
                    title: self.options.arrowSymbols[i],
                    type: 'image'
                })
                .addClass('OxArrow')
                .mousedown(function(e) {
                    clickArrow(e, i);
                })
                .appendTo(that.$element);
            });
        }

        self.$track = $('<div>')
            .addClass('OxTrack')
            .css($.extend({
                width: (self.trackSize - 2) + 'px'
            }, self.trackImages == 1 ? {
                background: 'rgb(0, 0, 0)'
            } : {}))
            .mousedown(clickTrack)
            .appendTo(that.$element);

        self.trackColors && setTrackColors();

        if (self.trackImages) {
            self.$trackImages = $('<div>')
                .css({
                    width: self.trackSize + 'px',
                    marginRight: (-self.trackSize - 1) + 'px'
                })
                .appendTo(self.$track);
            $.each(self.options.trackImages, function(i, v) {
                Ox.print(self.trackImageWidths[i])
                $('<img>')
                    .attr({
                        src: v
                    })
                    .addClass(i == 0 ? 'OxFirstChild' : '')
                    .addClass(i == self.trackImages - 1 ? 'OxLastChild' : '')
                    .css({
                        width: self.trackImageWidths[i] + 'px'
                    })
                    .mousedown(function(e) {
                        e.preventDefault(); // prevent drag
                    })
                    .appendTo(self.$trackImages);
                //left += self.trackImageWidths[i];
            });
        }

        self.$thumb = Ox.Button({
                id: self.options.id + 'Thumb',
                title: self.options.thumbValue ? (self.options.valueNames ?
                        self.options.valueNames[self.options.value] :
                        self.options.value) : '',
                width: self.thumbSize
            })
            .addClass('OxThumb')
            /*
            .css({
                border: '1px solid rgb(255, 255, 255)',
                background: 'rgba(0, 0, 0, 0)'
            })
            */
            .appendTo(self.$track);

        setThumb();

        function clickArrow(e, i) {
            // fixme: shift doesn't work, see menu scrolling
            var interval,
                timeout = setTimeout(function() {
                    interval = setInterval(function() {
                        setValue(self.options.value + self.options.arrowStep * (i == 0 ? -1 : 1));
                    }, 50);
                }, 500);
            setValue(self.options.value + self.options.arrowStep * (i == 0 ? -1 : 1) * (e.shiftKey ? 2 : 1), true);
            $window.one('mouseup', function() {
                clearInterval(interval);
                clearTimeout(timeout);
            });
        }

        function clickTrack(e) {
            //Ox.Focus.focus();
            var isThumb = $(e.target).hasClass('OxThumb'),
                left = self.$track.offset().left,
                offset = isThumb ? e.clientX - self.$thumb.offset().left - 8 /*self.thumbSize / 2*/ : 0;
            setValue(val(e), !isThumb);
            $window.mousemove(function(e) {
                setValue(val(e));
            });
            $window.one('mouseup', function() {
                $window.unbind('mousemove');
            });
            function val(e) {
                return getVal(e.clientX - left - offset);
            }
        }

        function getPx(val) {
            var pxPerVal = (self.trackSize - self.thumbSize) /
                        (self.options.max - self.options.min);
            return Math.ceil((val - self.options.min) * pxPerVal);
        }

        /*
        function getTime(oldValue, newValue) {
            return self.animationTime * Math.abs(oldValue - newValue) / (self.options.max - self.options.min);
        }
        */

        function getVal(px) {
            var px = self.trackSize / self.values >= 16 ? px : px - 8,
                valPerPx = (self.options.max - self.options.min) /
                        (self.trackSize - self.thumbSize);
            return Ox.limit(self.options.min +
                    Math.floor(px * valPerPx / self.options.step) * self.options.step,
                    self.options.min, self.options.max);
        }

        function setThumb(animate) {
            self.$thumb.stop().animate({
                marginLeft: (getPx(self.options.value) - 1) + 'px',
                //width: self.thumbSize + 'px'
            }, animate ? 200 : 0, function() {
                if (self.options.thumbValue) {
                    self.$thumb.options({
                        title: self.options.valueNames ?
                                self.options.valueNames[self.options.value] :
                                self.options.value
                    });
                }
            });
        }

        function setTrackColors() {
            self.$track.css({
                backgroundImage: $.browser.mozilla ?
                    ('-moz-linear-gradient(left, ' +
                    self.options.trackColors[0] + ' 0%, ' + $.map(self.options.trackColors, function(v, i) {
                        return v + ' ' + ((self.trackColorsStart + self.trackColorsStep * i) * 100) + '%';
                    }).join(', ') + ', ' + self.options.trackColors[self.trackColors - 1] + ' 100%)') :
                    ('-webkit-gradient(linear, left top, right top, color-stop(0, ' +
                    self.options.trackColors[0] + '), ' + $.map(self.options.trackColors, function(v, i) {
                        return 'color-stop(' + (self.trackColorsStart + self.trackColorsStep * i) + ', ' + v + ')';
                    }).join(', ') + ', color-stop(1, ' + self.options.trackColors[self.trackColors - 1] + '))')
            });
        }

        function setValue(value, animate) {
            var value = Ox.limit(value, self.options.min, self.options.max);
            if (value != self.options.value) {
                //time = getTime(self.options.value, value);
                self.options.value = value;
                setThumb(animate);
                that.triggerEvent('change', {
                    value: value
                });
            }
        }

        self.onChange = function(key, value) {
            if (key == 'trackColors') {
                setTrackColors();
            } else if (key == 'value') {
                setThumb();
            }
        }

        return that;

    };

    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: [],
                    max: 1,
                    min: 1,
                    overlap: 'none', // can be none, left or right
                    selectable: true,
                    size: 'medium',
                    title: '',
                    type: 'text', // can be 'text' or 'image'
                    width: 'auto'
                })
                .options(options)
                .addClass(
                    'OxSelect Ox' + Ox.toTitleCase(self.options.size) +
                    (self.options.overlap == 'none' ? '' : ' OxOverlap' +
                    Ox.toTitleCase(self.options.overlap))
                )
                .css(self.options.width == 'auto' ? {} : {
                    width: self.options.width + 'px'
                })
                .bindEvent({
                    key_escape: loseFocus,
                    key_down: showMenu
                });

        $.extend(self, {
            buttonId: self.options.id + 'Button',
            groupId: self.options.id + 'Group',
            menuId: self.options.id + 'Menu'
        });

        if (self.options.selectable) {
            self.optionGroup = new Ox.OptionGroup(
                self.options.items,
                self.options.min,
                self.options.max
            );
            self.options.items = self.optionGroup.init();
            self.checked = self.optionGroup.checked();
        }

        if (self.options.type == 'text') {
            self.$title = $('<div>')
                .addClass('OxTitle')
                .css({
                    width: (self.options.width - 22) + 'px'
                })
                .html(
                    self.options.title ? self.options.title :
                    self.options.items[self.checked[0]].title
                )
                .click(showMenu)
                .appendTo(that.$element);
        }

        self.$button = new Ox.Button({
                id: self.buttonId,
                style: 'symbol',
                title: 'select',
                type: 'image'
            })
            .bindEvent('click', showMenu)
            .appendTo(that);

        self.$menu = new Ox.Menu({
                element: self.$title || self.$button,
                id: self.menuId,
                items: [self.options.selectable ? {
                    group: self.groupId,
                    items: self.options.items,
                    max: self.options.max,
                    min: self.options.min
                } : self.options.items],
                side: 'bottom',
                size: self.options.size
            })
            .bindEvent({
                change: changeMenu,
                click: clickMenu,
                hide: hideMenu
            });

        self.options.type == 'image' && self.$menu.addClass('OxRight');

        function clickMenu(event, data) {
            Ox.print('%% clickMenu')
        }

        function changeMenu(event, data) {
            Ox.print('clickMenu: ', self.options.id, data)
            self.checked = self.optionGroup.checked();
            self.$title && self.$title.html(
                self.options.title ? self.options.title :
                        data.checked[0].title
            );
            that.triggerEvent('change', {
                selected: data.checked
            });
        }

        function hideMenu() {
            Ox.print('%% hideMenu that', that, 'self', self)
            that.removeClass('OxSelected');
            // self.$button.removeClass('OxSelected');
            Ox.print('%% hideMenu end')
        }

        function loseFocus() {
            that.loseFocus();
        }

        function showMenu() {
            that.gainFocus();
            that.addClass('OxSelected');
            self.$menu.showMenu();
        }

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

        that.selected = function() {
            return $.map(/*self.checked*/self.optionGroup.checked(), function(v, i) {
                return {
                    id: self.options.items[i].id,
                    title: self.options.items[i].title
                };
            });
        };

        that.selectItem = function(id) {
            Ox.print('selectItem', id, Ox.getObjectById(self.options.items, id).title)
            self.options.type == 'text' && self.$title.html(
                Ox.getObjectById(self.options.items, id).title[0] // fixme: title should not have become an array
            );
            self.$menu.checkItem(id);
            self.checked = self.optionGroup.checked();
        };

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

        return that;

    };

    Ox.FormElementGroup = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    id: '',
                    elements: [],
                    float: 'left',
                    separators: [],
                    width: 0
                })
                .options(options || {})
                .addClass('OxInputGroup');

        $.each(self.options.float == 'left' ? self.options.elements : self.options.elements.reverse(), function(i, $element) {
            $element.css({
                    float: self.options.float // fixme: make this a class
                })
                .bindEvent({
                    validate: function(event, data) {
                        that.triggerEvent({
                            validate: data
                        });
                    }
                })
                .appendTo(that);
        });

        /*
        if (self.options.width) {
            setWidths();
        } else {
            self.options.width = getWidth();
        }
        that.css({
            width: self.options.width + 'px'
        });
        */

        function getWidth() {
            
        }

        function setWidth() {
            
        }

        self.onChange = function(key, value) {
            if (key == 'trackColors') {
                
            }
        }

        that.value = function() {
            return $.map(self.options.elements, function(input) {
                var ret = null;
                ['checked', 'selected', 'value'].forEach(function(v) {
                    input[v] && (ret = input[v]());
                });
                return ret;
            });
        };

        return that;

    };

    Ox.Picker = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    element: null,
                    elementHeight: 128,
                    elementWidth: 256,
                    id: '',
                    overlap: 'none'
                })
                .options(options || {});

        self.$selectButton = new Ox.Button({
                overlap: self.options.overlap,
                title: 'select',
                type: 'image'
            })
            .click(showMenu)
            .appendTo(that);

        self.$menu = new Ox.Element('div')
            .addClass('OxPicker')
            .css({
                width: self.options.elementWidth + 'px',
                height: (self.options.elementHeight + 24) + 'px'
            });

        self.options.element
            .css({
                width: self.options.elementWidth + 'px',
                height: self.options.elementHeight + 'px'
            })
            .appendTo(self.$menu);

        self.$bar = new Ox.Bar({
                orientation: 'horizontal',
                size: 24
            })
            .appendTo(self.$menu);

        that.$label = new Ox.Label({
                width: self.options.elementWidth - 60
            })
            .appendTo(self.$bar);

        self.$doneButton = new Ox.Button({
                title: 'Done',
                width: 48
            })
            .click(hideMenu)
            .appendTo(self.$bar);

        self.$layer = $('<div>')
            .addClass('OxLayer')
            .click(hideMenu);

        function hideMenu() {
            self.$menu.detach();
            self.$layer.detach();
            self.$selectButton
                .removeClass('OxSelected')
                .css({
                    MozBorderRadius: '8px',
                    WebkitBorderRadius: '8px'
                });
            that.triggerEvent('hide');
        };

        function showMenu() {
            var offset = that.offset(),
                left = offset.left,
                top = offset.top + 15;
            self.$selectButton
                .addClass('OxSelected')
                .css({
                    MozBorderRadius: '8px 8px 0 0',
                    WebkitBorderRadius: '8px 8px 0 0'
                });
            self.$layer.appendTo($body);
            self.$menu
                .css({
                    left: left + 'px',
                    top: top + 'px'
                })
                .appendTo($body);
            that.triggerEvent('show');
        };

        return that;

    };

    Ox.ColorPicker = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    id: '',
                    value: '0, 0, 0'
                })
                .options(options || {});

        Ox.print(self)
        self.$ranges = [];
        self.rgb = ['red', 'green', 'blue'];
        self.values = self.options.value.split(', ');

        $.each(Ox.range(3), function(i) {
            self.$ranges[i] = new Ox.Range({
                    arrows: true,
                    id: self.options.id + Ox.toTitleCase(self.rgb[i]),
                    max: 255,
                    size: 328, // 256 + 16 + 40 + 16
                    thumbSize: 40,
                    thumbValue: true,
                    trackColors: getColors(i),
                    value: self.values[i]
                })
                .css({
                    position: 'absolute',
                    top: (i * 15) + 'px'
                })
                .bindEvent('change', function(event, data) {
                    change(i, data.value);
                })
                .appendTo(that);
            // fixme: make self.$ranges[i].children() work
            if (i == 0) {
                self.$ranges[i].$element.children('input.OxOverlapRight').css({
                    MozBorderRadius: 0,
                    WebkitBorderRadius: 0
                });
                self.$ranges[i].$element.children('input.OxOverlapLeft').css({
                    MozBorderRadius: '0 8px 0 0',
                    WebkitBorderRadius: '0 8px 0 0'
                });
            } else {
                self.$ranges[i].$element.children('input').css({
                    MozBorderRadius: 0,
                    WebkitBorderRadius: 0
                });
            }
        });

        that = new Ox.Picker({
            element: that,
            elementHeight: 46,
            elementWidth: 328,
            id: self.options.id
        });

        function change(index, value) {
            self.values[index] = value;
            self.options.value = self.values.join(', ');
            that.$label.css({
                background: 'rgb(' + self.options.value + ')'
            });
            $.each(Ox.range(3), function(i) {
                if (i != index) {
                    self.$ranges[i].options({
                        trackColors: getColors(i)
                    });
                }
            });
            that.triggerEvent('change', {
                value: self.options.value
            });
        }

        function getColors(index) {
            return [
                'rgb(' + $.map(Ox.range(3), function(v) {
                    return v == index ? 0 : self.values[v];
                }).join(', ') + ')',
                'rgb(' + $.map(Ox.range(3), function(v) {
                    return v == index ? 255 : self.values[v];
                }).join(', ') + ')'
            ]
        }

        return that;

    };

    Ox.PlacePicker = function(options, self) {

        var self = $.extend(self || {}, {
                options: $.extend({
                    id: '',
                    value: 'United States'
                }, options)
            }),
            that;

        self.$element = new Ox.Element('div')
            .css({
                width: '256px',
                height: '192px'
            })
            .append(
                self.$topBar = new Ox.Bar({
                        size: 16
                    })
                    .css({
                        MozBorderRadius: '0 8px 0 0',
                        WebkitBorderRadius: '0 8px 0 0'
                    })
                    .append(
                        self.$input = new Ox.Input({
                                clear: true,
                                id: self.options.id + 'Input',
                                placeholder: 'Find',
                                width: 256
                            })
                            .bindEvent('submit', findPlace)
                    )
            )
            .append(
                self.$container = new Ox.Element('div')
                    .css({
                        width: '256px',
                        height: '160px'
                    })
            )
            .append(
                self.$bottomBar = new Ox.Bar({
                        size: 16
                    })
                    .append(
                        self.$range = new Ox.Range({
                                arrows: true,
                                id: self.options.id + 'Range',
                                max: 22,
                                size: 256,
                                thumbSize: 32,
                                thumbValue: true
                            })
                            .bindEvent('change', changeZoom)
                    )
            );

        self.$input.$element.children('input[type=text]').css({
            width: '230px',
            paddingLeft: '2px',
            MozBorderRadius: '0 8px 8px 0',
            WebkitBorderRadius: '0 8px 8px 0'
        });
        self.$input.$element.children('input[type=image]').css({
            MozBorderRadius: '0 8px 0 0',
            WebkitBorderRadius: '0 8px 0 0'
        });
        self.$range.$element.children('input').css({
            MozBorderRadius: 0,
            WebkitBorderRadius: 0
        });

        that = new Ox.Picker({
                element: self.$element,
                elementHeight: 192,
                elementWidth: 256,
                id: self.options.id,
                overlap: self.options.overlap,
                value: self.options.value
            }, self)
            .bindEvent('show', showPicker);

        that.$label.bind('click', clickLabel)

        self.map = false;

        function changeZoom(event, data) {
            Ox.print('changeZoom')
            self.$map.zoom(data.value);
        }

        function clickLabel() {
            var name = that.$label.html();
            if (name) {
                self.$input.options({
                    value: name
                })
                .triggerEvent('submit', {
                    value: name
                });
            }
        }

        function findPlace(event, data) {
            Ox.print('findPlace', data);
            self.$map.find(data.value, function(place) {
                place && that.$label.html(place.geoname);
            })
        }

        function onSelect(event, data) {
            that.$label.html(data.geoname);
        }

        function onZoom(event, data) {
            self.$range.options({
                value: data.value
            });
        }

        function showPicker() {
            if (!self.map) {
                self.$map = new Ox.Map({
                        id: self.options.id + 'Map',
                        places: [self.options.value]
                    })
                    .css({
                        width: '256px',
                        height: '160px'
                    })
                    .bindEvent({
                        select: onSelect,
                        zoom: onZoom
                    })
                    .appendTo(self.$container);
                self.map = true;
            }
        }

        return that;

    };

    /**
    delete below
    */

    Ox.Input_ = function(options, self) {

        /*
        options:
            clear                   boolean, clear button, or not
            disabled                boolean, disabled, or not
            height                  height (px), if type is 'textarea'
            id
            label                   string, or
                                    array [{ id, title, checked }] (selectable label) or
                                    array [{ id, label: [{ id, title, checked }], width }] (multiple selectable labels)
                                    label and placeholder are mutually exclusive
            labelWidth              integer (px)
            placeholder             string, or
                                    array [{ id, title, checked }] (selectable placeholder)
                                    label and placeholder are mutually exclusive
            separator               string, or
                                    array of strings
                                    to separate multiple values
            separatorWidth          integer (px), or
                                    array of integers
            serialize               function
            size                    'large', 'medium' or 'small'
            type                    'password', 'select' or 'text'
            unit                    string, or
                                    array [{ id, title, checked }] (selectable unit)
            unitWidth               integer (px)
            value                   string, or
                                    array [{ id, value, width }] (multiple values)
            width                   integer (px)
        methods:
        events:
        */

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    autocomplete: null,
                    autocorrect: null,
                    autosuggest: null,
                    autosuggestHighlight: false,
                    autosuggestSubmit: false,
                    autovalidate: null,
                    autovalidateName: 'Value',
                    clear: false,
                    disabled: false,
                    height: 128,
                    id: '',
                    key: '',
                    label: '',
                    labelWidth: 64,
                    placeholder: '',
                    separator: '',
                    separatorWidth: 16,
                    serialize: null,
                    size: 'medium',
                    type: 'text',
                    unit: '',
                    unitWidth: 64,
                    value: '',
                    width: 128
                })
                .options(options || {})
                .addClass('OxInput Ox' + Ox.toTitleCase(self.options.size))
                .css({
                    width: self.options.width + 'px'
                });

        $.extend(self, {
            clearWidth: 16,
            hasMultipleKeys: Ox.isArray(self.options.label) && 'label' in self.options.label[0],
            hasMultipleValues: Ox.isArray(self.options.value) &&
                    (self.options.type != 'select' || 'items' in self.options.value[0]),
            hasSelectableKeys: Ox.isArray(self.options.label) || Ox.isArray(self.options.placeholder),
            hasSelectableUnits: Ox.isArray(self.options.unit),
            keyName: self.options.label ? 'label' : (self.options.placeholder ? 'placeholder' : ''),
            placeholderWidth: 16,
            selectedKey: [0], // fixme: only set on demand?
            selectedValue: 0,
            selectedUnit: 0,
            /* valid: autovalidateCall(true) */
        });

        $.each(['autocomplete', 'autocorrect', 'autosuggest', 'autovalidate'], function(i, v) {
            //if (!Ox.isFunction(self.options[v])) {
                self.options[v] = {
                    '': self.options[v]
                };
            //}
        });

        if (self.keyName && !self.hasMultipleKeys) {
            self.options[self.keyName] = [$.extend({
                id: '',
                label: self.options[self.keyName],
            }, self.keyName == 'label' ? {
                id: '',
                width: self.options.labelWidth
            } : {})];
            if (!self.hasSelectableKeys) {
                self.options[self.keyName][0].label = [{
                    id: '',
                    title: self.options[self.keyName][0].label
                }];
            }
        }
        if (self.hasSelectableKeys) {
            $.each(self.options[self.keyName], function(keyPos, key) {
                if (key.width) {
                    self.options.labelWidth = (keyPos == 0 ? 0 : self.options.labelWidth) + key.width;
                }
                self.selectedKey[keyPos] = 0;
                $.each(key, function(valuePos, value) {
                    if (value.checked) {
                        self.selectedKey[keyPos] = valuePos;
                        return false;
                    }
                });
            });
        }
        self.valueWidth = self.options.width -
                (self.options.label ? self.options.labelWidth : 0) -
                ((self.options.placeholder && self.options.placeholder[0].label.length > 1) ? self.placeholderWidth : 0) -
                (self.options.unit ? self.options.unitWidth : 0) -
                (self.options.clear ? self.clearWidth : 0);
        /*
        if (self.hasMultipleValues) {
            self.valueWidth -= Ox.isArray(self.options.separatorWidth) ?
                    Ox.sum(self.options.separatorWidth) :
                    (self.options.value.length - 1) * self.options.separatorWidth;
        }
        */
        Ox.print('self.hasMulVal', self.hasMultipleValues);
        Ox.print('self.options.value', self.options.value)
        if (!self.hasMultipleValues) {
            if (self.options.type == 'select') {
                self.options.value = [{
                    id: '',
                    items: self.options.value,
                    width: self.valueWidth
                }];
            } else if (self.options.type == 'range') {
                self.options.value = [$.extend({
                    id: '',
                    size: self.valueWidth
                }, self.options.value)];
            } else {
                self.options.value = [{
                    id: '',
                    value: self.options.value,
                    width: self.valueWidth
                }]
            }
        }
        Ox.print('self.options.value', self.options.value)
        self.values = self.options.value.length;
        Ox.print(self.options.id, 'self.values', self.values)

        if (Ox.isString(self.options.separator)) {
            self.options.separator = $.map(new Array(self.values - 1), function(v, i) {
                return self.options.separator;
            });
        }
        if (Ox.isNumber(self.options.separatorWidth)) {
            self.options.separatorWidth = $.map(new Array(self.values - 1), function(v, i) {
                return self.options.separatorWidth;
            });
        }

        if (self.options.unit) {
            if (self.hasSelectableUnits) {
                $.each(self.options.unit, function(pos, unit) {
                    if (unit.checked) {
                        self.selectedUnit = pos;
                        return false;
                    }
                });
            } else {
                self.options.unit = [{
                    id: '',
                    title: self.options.unit
                }];
            }
        }

        Ox.print('self', self);

        if (self.keyName) {
            that.$key = [];
            $.each(self.options[self.keyName], function(keyPos, key) {
                Ox.print('keyPos key', keyPos, key)
                if (self.keyName == 'label' && key.label.length == 1) {
                    that.$key[keyPos] = new Ox.Label({
                            overlap: 'right',
                            title: key.label[0].title,
                            width: self.options.labelWidth
                        })
                        .css({
                            float: 'left'
                        })
                        .click(function() {
                            that.$input[0].focus();
                        })
                        .appendTo(that);
                } else if (key.label.length > 1) {
                    Ox.print('key.length > 1')
                    self.selectKeyId = self.options.id + Ox.toTitleCase(self.keyName) +
                            (self.options[self.keyName].length == 1 ? '' : keyPos);
                    Ox.print('three', self.selectedKey, keyPos, self.selectedKey[keyPos]);
                    that.$key[keyPos] = new Ox.Select({
                            id: self.selectKeyId,
                            items: $.map(key.label, function(value, valuePos) {
                                return {
                                    checked: valuePos == self.selectedKey[keyPos],
                                    id: value.id,
                                    group: self.selectKeyId, // fixme: same id, works here, but should be different
                                    title: value.title
                                };
                            }),
                            overlap: 'right',
                            type: self.options.label ? 'text' : 'image',
                            width: self.options.label ? (self.options.label.length == 1 ? self.options.labelWidth : key.width) : self.placeholderWidth
                        })
                        .css({
                            float: 'left'
                        })
                        .appendTo(that);
                    that.bindEvent('change_' + self.selectKeyId, changeKey);
                }
            });
        }

        if (self.options.clear) {
            that.$clear = new Ox.Button({
                    overlap: 'left',
                    type: 'image',
                    value: 'clear'
                })
                .css({
                    float: 'right'
                })
                .click(clear)
                .appendTo(that);
        }

        if (self.options.unit.length == 1) {
            that.$unit = new Ox.Label({
                    overlap: 'left',
                    title: self.options.unit[0].title,
                    width: self.options.unitWidth
                })
                .css({
                    float: 'right'
                })
                .click(function() {
                    that.$input[0].focus();
                })
                .appendTo(that);
        } else if (self.options.unit.length > 1) {
            self.selectUnitId = self.options.id + 'Unit';
            that.$unit = new Ox.Select({
                    id: self.selectUnitId,
                    items: $.map(self.options.unit, function(unit, i) {
                        Ox.print('unit', unit)
                        return {
                            checked: i == 0,
                            id: unit.id,
                            group: self.selectUnitId, // fixme: same id, works here, but should be different
                            title: unit.title
                        };
                    }),
                    overlap: 'left',
                    size: self.options.size,
                    width: self.options.unitWidth
                })
                .css({
                    float: 'right'
                })
                .appendTo(that);
        }

        if (self.values) {
            that.$separator = [];
            $.each(self.options.value, function(i, v) {
                if (i < self.values - 1) {
                    that.$separator[i] = new Ox.Label({
                            textAlign: 'center',
                            title: self.options.separator[i],
                            width: self.options.separatorWidth[i] + 32
                        })
                        .css({
                            float: 'left',
                            marginLeft: (v.width - (i == 0 ? 16 : 32)) + 'px'
                        })
                        .click(function() {
                            that.$input[0].focus();
                        })
                        .appendTo(that);
                }
            });
        }
        that.$input = [];
        //self.margin = 0;
        $.each(self.options.value, function(i, v) {
            Ox.print('o k i', self.options, self.keyName, i);
            var id = self.keyName ? $.map(self.selectedKey, function(v, i) {
                    return self.options[self.keyName][i].id;
                }).join('.') : '';
            //self.margin -= (i == 0 ? 16 : self.options.value[i - 1].width)
            Ox.print('v:', v, 'id:', id)
            if (self.options.type == 'select') {
                that.$input[i] = new Ox.Select({
                        id: v.id,
                        items: v.items,
                        width: v.width
                    }).
                    css({
                        float: 'left'
                    });
            } else if (self.options.type == 'range') {
                that.$input[i] = new Ox.Range(v)
                    .css({
                        float: 'left'
                    });
            } else {
                that.$input[i] = new Ox.InputElement({
                    autocomplete: self.options.autocomplete[id],
                    autocorrect: self.options.autocorrect[id],
                    autosuggest: self.options.autosuggest[id],
                    autosuggestHighlight: self.options.autosuggestHighlight,
                    autosuggestSubmit: self.options.autosuggestSubmit,
                    autovalidate: self.options.autovalidate[id],
                    autovalidateName: self.options.autovalidateName,
                    disabled: self.options.disabled,
                    height: self.options.height,
                    id: self.options.id + 'Input' + Ox.toTitleCase(v.id),
                    key: self.hasSelectableKeys ? self.options[self.keyName][0].label[self.selectedKey[0]].id : '',
                    parent: that,
                    placeholder: self.options.placeholder ? self.options.placeholder[0].label[0].title : '',
                    size: self.options.size,
                    type: self.options.type,
                    value: v.value,
                    width: v.width
                });
            }
            that.$input[i]
                .css($.extend({}, self.options.value.length > 1 ? {
                    float: 'left',
                    marginLeft: -Ox.sum($.map(self.options.value, function(v_, i_) {
                        return i_ > i ? self.options.value[i_ - 1].width + self.options.separatorWidth[i_ - 1] : (i_ == i ? 16 : 0);
                    }))
                } : {}))
                .appendTo(that);
        });

        //width(self.options.width);

        function changeKey(event, data) {
            Ox.print('changeKey', data);
            if (data) { // fixme: necessary?
                self.key = {
                    id: data.id,
                    title: data.value // fixme: should be data.title
                };
                that.$input[0].options({
                    key: data.id
                });
            }
            if (self.options.label) {
                //that.$label.html(self.option.title);
                that.$input[0].focus();
                //autocompleteCall();
            } else {
                that.$input[0].options({
                    placeholder: data.value // fixme: should be data.title
                });
                /*
                if (that.$input.hasClass('OxPlaceholder')) {
                    that.$input.val(self.key.title);
                    //that.$input.focus();
                } else {
                    that.$input.focus();
                    self.options.autosuggest && autosuggestCall();
                }
                */
            }
        }

        function changeUnit() {
            that.$input[0].focus();
        }

        function clear() {
            $.each(that.$input, function(i, v) {
                v.val('');
            });
            that.$input[0].focus();
        }

        function height(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)))'
                    });
            }
        }

        function selectUnit() {
            self.$selectUnitMenu.show();
        }

        function submit() {
            Ox.print('submit')
            var value = that.$input.val();
            that.$input.blur();
            that.triggerEvent('submit', self.options.key ? {
                key: self.options.key,
                value: value
            } : value);
        }

        function width(value) {
            that.$element.width(value);
            that.$input.width(
                value - (self.options.type == 'textarea' ? 0 : 12) -
                        (self.options.label ? self.options.labelWidth : 0) -
                        (self.options.placeholder.length > 1 ? 16 : 0) -
                        (self.options.unit ? self.options.unitWidth : 0) -
                        (self.options.clear ? 16 : 0)
            );
        }

        self.onChange = function(key, value) {
            if (key == 'height') {
                height(value);
            } else if (key == 'width') {
                width(value);
            }
        };

        that.changeLabel = function(id) {
            that.$key.html(Ox.getObjectById(self.options.label, id).title);
            self.selectMenu.checkItem(id);
        };

        return that;

    }

    Ox.InputElement_ = function(options, self) {

        var self = self || {},
            that = new Ox.Element(
                    options.type == 'textarea' ? 'textarea' : 'input', self
                )
                .defaults({
                    autocomplete: null,
                    autocorrect: null,
                    autosuggest: null,
                    autosuggestHighlight: false,
                    autosuggestSubmit: false,
                    autovalidate: null,
                    disabled: false,
                    height: 128,
                    id: '',
                    key: '',
                    parent: null,
                    placeholder: '',
                    size: 'medium',
                    type: 'text',
                    value: '',
                    width: 128
                })
                .options(options || {})
                .addClass('OxInput Ox' + Ox.toTitleCase(self.options.size) + (
                    (self.options.placeholder && self.options.value === '') ?
                    ' OxPlaceholder' : ''
                ))
                .attr(self.options.type == 'textarea' ? {} : {
                    type: self.options.type
                })
                .css({
                    float: 'left',
                    width: (self.options.width - 14) + 'px'
                })
                .val(
                    (self.options.placeholder && self.options.value === '') ?
                    self.options.placeholder : self.options.value
                )
                .blur(blur)
                .change(change)
                .focus(focus);

        Ox.print('InputElement self.options', self.options)

        self.bindKeyboard = self.options.autocomplete || self.options.autocorrect ||
                        self.options.autosuggest || self.options.autovalidate;

        if (self.options.autosuggest) {
            self.autosuggestId = self.options.id + 'Menu'; // fixme: we do this in other places ... are we doing it the same way? var name?
            self.$autosuggestMenu = new Ox.Menu({
                element: that.$element,
                id: self.autosuggestId,
                offset: {
                    left: 4,
                    top: 0
                },
                size: self.options.size
            });
            that.bindEvent('click_' + self.autosuggestId, clickMenu);
        }

        that.bindEvent($.extend(self.options.type == 'textarea' ? {} : {
            key_enter: submit
        }, {
            key_escape: cancel
        }));

        function autocomplete(value) {
            var value = value.toLowerCase(),
                ret = '';
            if (value !== '') {
                $.each(self.options.autocomplete, function(i, v) {
                    if (v.toLowerCase().indexOf(value) == 0) {
                        ret = v;
                        return false;
                    }
                });
            }
            return ret;
        }

        function autocompleteCall() {
            var value = that.$element.val();
            Ox.isFunction(self.options.autocomplete) ?
                self.options.autocomplete(self.options.key ? {
                    key: self.options.key,
                    value: value
                } : value, autocompleteCallback) :
                autocompleteCallback(autocomplete(value));
        }

        function autocompleteCallback(value) {
            var pos = cursor()[0];
            if (value) {
                that.$element.val(value);
                cursor(pos, value.length);
            }
        }

        function autocorrect(value) {
            var length = value.length;
            return $.map(value.toLowerCase().split(''), function(v, i) {
                if (new RegExp(self.options.autocorrect)(v)) {
                    return v
                } else {
                    return null;
                }
            }).join('');
        }

        function autocorrectCall(blur) {
            var blur = blur || false,
                value = that.$element.val(),
                pos = cursor()[0];
            Ox.isFunction(self.options.autocorrect) ?
                self.options.autocorrect(value, blur, autocorrectCallback) :
                autocorrectCallback(autocorrect(value), blue);
        }

        function autocorrectCallback(value, blur) {
            var length = that.$element.val().length;
            that.$element.val(self.options.value);
            !blur && cursor(pos + value.length - length);
        }

        function autosuggest(value) {
            var value = value.toLowerCase(),
                values = [[], []];
            if (value !== '') {
                $.each(self.options.key ? self.options.autosuggest[self.options.key] : self.options.autosuggest, function(i, v) {
                    //Ox.print('v...', v)
                    var index = v.toLowerCase().indexOf(value);
                    index > -1 && values[index == 0 ? 0 : 1].push(v);
                });
            }
            return $.merge(values[0], values[1]);
        }

        function autosuggestCall() {
            var value = that.$element.val();
            Ox.isFunction(self.options.autosuggest) ?
                self.options.autosuggest(self.options.key ? {
                    key: self.options.key,
                    value: value
                } : value, autosuggestCallback) :
                autosuggestCallback(autosuggest(value));
        }

        function autosuggestCallback(values) {
            var values = values || [],
                selected = values.length == 1 ? 0 : -1,
                value = that.$element.val().toLowerCase();
            //Ox.print('values', values);
            if (values.length) {
                values = $.map(values, function(v, i) {
                    if (value == v.toLowerCase()) {
                        selected = i;
                    }
                    return {
                        id: v.toLowerCase().replace(/ /g, '_'), // fixme: need function to do lowercase, underscores etc?
                        title: self.options.autosuggestHighlight ? v.replace(
                            new RegExp('(' + value + ')', 'ig'),
                            '<span class="OxHighlight">$1</span>'
                        ) : v
                    };
                });
                // self.selectMenu && self.selectMenu.hideMenu(); // fixme: need event
                self.$autosuggestMenu.options({
                    items: values,
                    selected: selected
                }).showMenu();
            } else {
                self.$autosuggestMenu.hideMenu();
            }
        }

        function autovalidate(value) {
            return {
                valid: self.options.autovalidate(value) != null,
                message: 'Invalid ' + self.options.name
            };
        }

        function autovalidateCall(blur) {
            var blur = blur || false,
                value = that.$element.val();
            if (value !== '') {
                Ox.isFunction(self.options.autovalidate) ?
                    self.options.autovalidate(value, autovalidateCallback) :
                    autovalidateCallback(autovalidate(value), blur);
            } else {
                autovalidateCallback({
                    blur: blur,
                    valid: false,
                    message: 'Empty ' + self.options.name
                });
            }
        }

        function autovalidateCallback(data, blur) {
            if (data.valid != self.valid) {
                self.valid = data.valid;
                that.triggerEvent('validate', $.extend(data, {
                    blur: blur
                }));
            }
        }

        function blur() {
            if (!self.options.autosuggest || self.$autosuggestMenu.is(':hidden')) {
                Ox.print('losing focus...')
                that.loseFocus();
                self.options.parent.removeClass('OxFocus');
                self.options.autocorrect && autocorrectCall(true);
                // self.options.autosuggest && self.$autosuggestMenu.hideMenu();
                self.options.autovalidate && autovalidateCall(true);
                if (self.options.placeholder && that.$element.val() === '') {
                    that.$element.addClass('OxPlaceholder').val(self.options.placeholder);
                }
            }
            if (self.bindKeyboard) {
                $document.unbind('keydown', keypress);
                $document.unbind('keypress', keypress);
            }
        }

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

        function change() {
            
        }

        function clear() {
            that.$element.val('').focus();
        }

        function clickMenu(event, data) {
            Ox.print('clickMenu', data);
            that.$element.val(data.title);
            //self.$autosuggestMenu.hideMenu();
            self.options.autosuggestSubmit && submit();
        }

        function cursor(start, end) {
            /*
            cursor()                returns [start, end]
            cursor(start)           sets start
            cursor([start, end])    sets start and end
            cursor(start, end)      sets start and end
            */
            var isArray = Ox.isArray(start);
            if (arguments.length == 0) {
                return [that.$element[0].selectionStart, that.$element[0].selectionEnd];
            } else {
                start = isArray ? start[0] : start;
                end = isArray ? start[1] : (end ? end : start);
                that.$element[0].setSelectionRange(start, end);
            }
        }

        function focus() {
            var val = that.$element.val();
            that.gainFocus();
            self.options.parent.addClass('OxFocus');
            if (that.$element.hasClass('OxPlaceholder')) {
                that.$element.val('').removeClass('OxPlaceholder');
            }
            if (self.bindKeyboard) {
                // fixme: different in webkit and firefox (?), see keyboard handler, need generic function
                $document.keydown(keypress);
                $document.keypress(keypress);
                Ox.print('calling autosuggest...')
                self.options.autosuggest && setTimeout(autosuggestCall, 0); // fixme: why is the timeout needed?
            }
        }

        function keypress(event) {
            Ox.print('keyCode', event.keyCode)
            if (event.keyCode != 9 && event.keyCode != 13 && event.keyCode != 27) { // fixme: can't 13 and 27 return false?
                setTimeout(function() { // fixme: document what this timeout is for
                    var value = that.$element.val();
                    if (value != self.options.value) {
                        self.options.value = value;
                        self.options.autocomplete && autocompleteCall();
                        self.options.autocorrect && autocorrectCall();
                        self.options.autosuggest && autosuggestCall();
                        self.options.autovalidate && autovalidateCall();
                    }
                }, 25);
            }
        }

        function submit() {
            
        }

        self.onChange = function(key, value) {
            if (key == 'placeholder') {
                that.$element.hasClass('OxPlaceholder') && that.$element.val(value);
            } else if (key == 'value') {
                if (self.options.placeholder) {
                    if (value === '') {
                        that.$element.addClass('OxPlaceholder').val(self.options.placeholder);
                    } else {
                        that.$element.removeClass('OxPlaceholder');
                    }
                }
                change(); // fixme: keypress too
            }
        }

        return that;

    }

    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,
                    valueNames: null
                })
                .options($.extend(options, {
                    arrowStep: options.arrowStep ?
                        options.arrowStep : options.step,
                    trackImages: $.makeArray(options.trackImages || [])
                }))
                .addClass('OxRange')
                .css({
                    width: self.options.size + 'px'
                });

        // 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);

        if (trackImages) {
            var width = parseFloat(screen.width / trackImages),
                $image = $('<canvas>')
                    .attr({
                        width: width * trackImages,
                        height: 14
                    })
                    .addClass('OxImage')
                    .appendTo($track.$element),
                c = $image[0].getContext('2d');
                c.mozImageSmoothingEnabled = false; // we may want to remove this later
            $.each(self.options.trackImages, function(i, v) {
                var left = 0;
                $('<img/>')
                    .attr({
                        src: v
                    })
                    .load(function() {
                        c.drawImage(this, left, 0, self.trackImageWidth[i], 14);
                    });
                left += self.trackImageWidth[i];
            });
        }
        var $thumb = Ox.Button({})
            .addClass('OxThumb')
            .appendTo($track);
            Ox.print('----')
        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.valueNames ?
                                self.options.valueNames[self.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(key, value) {
            
        }

        return that;

    };

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

    Ox.IconList = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    centerSelection: false,
                    draggable: true,
                    id: '',
                    item: function() {},
                    keys: [],
                    max: -1,
                    min: 0,
                    orientation: 'both',
                    request: function() {},
                    selected: [],
                    size: 128,
                    sort: [],
                })
                .options(options || {});

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

        that.$element = new Ox.List({
                centered: self.options.centered,
                construct: constructItem,
                draggable: self.options.draggable,
                id: self.options.id,
                itemHeight: self.itemHeight,
                itemWidth: self.itemWidth,
                keys: self.options.keys,
                orientation: self.options.orientation,
                keys: self.options.keys,
                max: self.options.max,
                min: self.options.min,
                request: self.options.request,
                selected: self.options.selected,
                size: self.options.size,
                sort: self.options.sort,
                type: 'icon',
                unique: self.options.unique
            }, $.extend({}, self)) // pass event handler
            .addClass('OxIconList Ox' + Ox.toTitleCase(self.options.orientation))
            .click(click)
            .dblclick(dblclick)
            .scroll(scroll);

        updateKeys();

        function click() {
            
        }

        function constructItem(data) {
            var data = !$.isEmptyObject(data) ? 
                    self.options.item(data, self.options.sort, self.options.size) :
                    {height: 8, width: 5},
                ratio = data.width / data.height;
            return new Ox.IconItem($.extend(data, {
                height: Math.round(self.options.size / (ratio <= 1 ? 1 : ratio)),
                size: self.options.size,
                width: Math.round(self.options.size * (ratio >= 1 ? 1 : ratio))
            }));
        }

        function dblclick() {
            
        }

        function scroll() {
            
        }

        function updateKeys() {
            self.options.keys = Ox.unique($.merge(self.options.keys, [self.options.sort[0].key]));
            that.$element.options({
                keys: self.options.keys
            });
        }

        self.onChange = function(key, value) {
            if (key == 'request') {
                that.$element.options(key, value);
            } else if (key == 'selected') {
                that.$element.options(key, value);
            }
        }

        that.closePreview = function() {
            that.$element.closePreview();
        };

        that.scrollToSelection = function() {
            that.$element.scrollToSelection();
        };

        that.size = function() {
            that.$element.size();
        }

        that.sortList = function(key, operator) {
            self.options.sort = [{
                key: key,
                operator: operator
            }];
            updateKeys();
            that.$element.sortList(key, operator);
        }

        return that;

    };

    Ox.IconItem = function(options, self) {

        //Ox.print('IconItem', options, self)

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

        $.extend(self, {
            fontSize: self.options.size == 64 ? 6 : 9,
            height: self.options.size * 1.5,
            lineLength: self.options.size == 64 ? 15 : 23,
            lines: self.options.size == 64 ? 4 : 5,
            url: oxui.path + '/png/ox.ui/transparent.png',
            width: self.options.size
        });
        self.title = formatText(self.options.title, self.lines - 1, self.lineLength);
        self.info = formatText(self.options.info, 5 - self.title.split('<br/>').length, self.lineLength);

        that.css({
            width: self.width + 'px',
            height: self.height + 'px'
        });
        that.$icon = $('<div>')
            .addClass('OxIcon')
            .css({
                top: self.options.size == 64 ? -64 : -124,
                width: (self.options.size + 4) + 'px',
                height: (self.options.size + 4) + 'px'
            });
        that.$iconImage = $('<img>')
            .addClass('OxLoading OxTarget')
            .attr({
                src: self.url
            })
            .css({
                width: self.options.width + 'px',
                height: self.options.height + 'px'
            })
            .mousedown(mousedown)
            .mouseenter(mouseenter)
            .mouseleave(mouseleave);
        self.options.url && that.$iconImage.one('load', load);
        that.$textBox = $('<div>')
            .addClass('OxText')
            .css({
                top: (self.options.size / 2) + 'px',
                width: (self.options.size + 4) + 'px',
                height: (self.options.size == 64 ? 30 : 58) + 'px'
            })
        that.$text = $('<div>')
            .addClass('OxTarget')
            .css({
                fontSize: self.fontSize + 'px'
            })
            .html(
                self.title + '<br/><span class="OxInfo">' + self.info + '</span>'
            )
            .mouseenter(mouseenter)
            .mouseleave(mouseleave);
        that.$reflection = $('<div>')
            .addClass('OxReflection')
            .css({
                top: self.options.size + 'px',
                width: (self.options.size + 4) + 'px',
                height: (self.options.size / 2) + 'px'
            });
        that.$reflectionImage = $('<img>')
            .addClass('OxLoading')
            .attr({
                src: self.url
            })
            .css({
                width: self.options.width + 'px',
                height: self.options.height + 'px',
                // firefox is 1px off when centering images with odd width and scaleY(-1)
                paddingLeft: ($.browser.mozilla && self.options.width % 2 ? 1 : 0) + 'px'
            });
        that.$gradient = $('<div>')
            .css({
                //top: (-self.options.size / 2) + 'px',
                width: self.options.width + 'px',
                height: (self.options.size / 2) + 'px'
            });

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

        function formatText(text, maxLines, maxLength) {
            var lines = Ox.wordwrap(text, maxLength, '<br/>', true, false).split('<br/>');
            return $.map(lines, function(line, i) {
                if (i < maxLines - 1) {
                    return line;
                } else if (i == maxLines - 1) {
                    return lines.length == maxLines ? line : Ox.truncate($.map(lines, function(line, i) {
                        return i < maxLines - 1 ? null : line;
                    }).join(' '), maxLength, '...', 'center');
                } else {
                    return null;
                }
            }).join('<br/>');
        }

        function load() {
            that.$iconImage.attr({
                    src: self.options.url
                })
                .one('load', function() {
                    that.$iconImage.removeClass('OxLoading');
                    that.$reflectionImage
                        .attr({
                            src: self.options.url
                        })
                        .removeClass('OxLoading');
                });
        }

        function mousedown(e) {
            // fixme: preventDefault keeps image from being draggable in safari - but also keeps the list from getting focus
            // e.preventDefault();
        }

        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({
                    centered: false,
                    construct: function() {},
                    draggable: false,
                    format: [],
                    itemHeight: 16,
                    itemWidth: 16,
                    keys: [],
                    max: -1,
                    min: 0,
                    orientation: 'vertical',
                    pageLength: 100,
                    request: function() {}, // (data, callback), without data returns {items, size etc.}
                    selected: [],
                    sort: [],
                    sortable: false,
                    type: 'text',
                    unique: ''
                })
                .options(options || {})
                .scroll(scroll);

        that.$content.mousedown(mousedown);

        $.extend(self, {
            $items: [],
            $pages: [],
            clickTimeout: 0,
            dragTimeout: 0,
            format: {},
            ids: [],
            itemMargin: self.options.type == 'text' ? 0 : 8, // 2 x 4 px margin ... fixme: the 2x should be computed later
            keyboardEvents: {
                key_end: scrollToFirst,
                key_enter: open,
                key_home: scrollToLast,
                key_pagedown: scrollPageDown,
                key_pageup: scrollPageUp,
                key_section: preview, // fixme: firefox gets keyCode 0 when pressing space
                key_space: preview
            },
            listMargin: self.options.type == 'text' ? 0 : 8, // 2 x 4 px padding
            page: 0,
            preview: false,
            requests: [],
            scrollTimeout: 0,
            selected: []
        });
        if (self.options.max == -1) {
            $.extend(self.keyboardEvents, {
                key_alt_control_a: invertSelection,
                key_control_a: selectAll
            });
        }
        if (self.options.min == 0) {
            $.extend(self.keyboardEvents, {
                key_control_shift_a: selectNone
            });
        }
        self.keyboardEvents[
            'key_' + (self.options.orientation == 'vertical' ? 'up' : 'left')
        ] = selectPrevious;
        self.keyboardEvents[
            'key_' + (self.options.orientation == 'vertical' ? 'down' : 'right')
        ] = selectNext;
        if (self.options.max == -1) {
            self.keyboardEvents[
                'key_' + (self.options.orientation == 'vertical' ? 'shift_up' : 'shift_left')
            ] = addPreviousToSelection;
            self.keyboardEvents[
                'key_' + (self.options.orientation == 'vertical' ? 'shift_down' : 'shift_right')
            ] = addNextToSelection;
        }
        if (self.options.orientation == 'both') {
            $.extend(self.keyboardEvents, {
                key_down: selectBelow,
                key_up: selectAbove
            });
            if (self.options.max == -1) {
                $.extend(self.keyboardEvents, {
                    key_shift_down: addBelowToSelection,
                    key_shift_up: addAboveToSelection
                });
            }
            self.pageLengthByRowLength = [
                0, 60, 60, 60, 60, 60, 60, 63, 64, 63, 60, 66, 60, 65, 70, 60, 64, 68, 72, 76, 60
            ];
        }
        if (self.options.draggable) {
            that.bind({
                dragstart: function(e) {
                    Ox.print('DRAGSTART', e);
                }
            });
        }

        updateQuery(self.options.selected);
        that.bindEvent(self.keyboardEvents);
        $window.resize(that.size);

        function addAboveToSelection() {
            var pos = getAbove();
            if (pos > -1) {
                addToSelection(pos);
                scrollToPosition(pos);
            }
        }

        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 addBelowToSelection() {
            var pos = getBelow();
            if (pos > -1) {
                addToSelection(pos);
                scrollToPosition(pos);
            }
        }

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

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

        function addToSelection(pos) {
            if (!isSelected(pos)) {
                self.selected.push(pos);
                !Ox.isUndefined(self.$items[pos]) &&
                    self.$items[pos].addClass('OxSelected');
                Ox.print('addToSelection')
                triggerSelectEvent();
            } else {
                // allow for 'cursor navigation' if orientation == 'both'
                self.selected.splice(self.selected.indexOf(pos), 1);
                self.selected.push(pos);
                Ox.print('self.selected', self.selected)
            }
        }

        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 constructEmptyPage(page) {
            Ox.print('cEP', page)
            var i, $page = new Ox.ListPage().css(getPageCSS(page));
            for (i = 0; i < (page < self.pages - 1 ?
                self.pageLength : self.listLength % self.pageLength); i++
            ) {
                // fixme: why does chainging fail here?
                new Ox.ListItem({
                    construct: self.options.construct
                }).appendTo($page);    
            }
            Ox.print('cEP done')
            return $page;
        }

        function deselect(pos) {
            if (isSelected(pos)) {
                self.selected.splice(self.selected.indexOf(pos), 1);
                !Ox.isUndefined(self.$items[pos]) &&
                    self.$items[pos].removeClass('OxSelected');
                triggerSelectEvent();
            }
        }

        function dragItem(pos, e) {
            var $item = self.$items[pos],
                id = self.ids[pos],
                startPos = pos,
                startY = e.clientY,
                stopPos = startPos,
                offsets = $.map(self.$items, function($item, pos) {
                    return (pos - startPos) * 16 - e.offsetY + 8;
                });
            Ox.print('dragItem', e);
            Ox.print(e.offsetY, offsets)
            $item.addClass('OxDrag') // fixme: why does the class not work?
                .css({
                    cursor: 'move',
                    opacity: 0.5
                });
            $window.mousemove(function(e) {
                var clientY = e.clientY - that.offset()['top'],
                    offset = clientY % 16,
                    position = Ox.limit(parseInt(clientY / 16), 0, self.$items.length - 1);
                if (position < pos) {
                    stopPos = position + (offset > 8 ? 1 : 0);
                } else if (position > pos) {
                    stopPos = position - (offset <= 8 ? 1 : 0);
                }
                if (stopPos != pos) {
                    moveItem(pos, stopPos);
                    pos = stopPos;
                }
            });
            $window.one('mouseup', function() {
                dropItem(id, pos);
                $window.unbind('mousemove');
            });
        }

        function moveItem(startPos, stopPos) {
            var $item = self.$items[startPos],
                insert = startPos < stopPos ? 'insertAfter' : 'insertBefore';
            $item.detach()[insert](self.$items[stopPos].$element); // fixme: why do we need .$element here?
            Ox.print('moveItem', startPos, stopPos, insert, self.ids);
            var $item = self.$items.splice(startPos, 1)[0];
                id = self.ids.splice(startPos, 1)[0];
            self.$items.splice(stopPos, 0, $item);
            self.ids.splice(stopPos, 0, id);
            self.$items.forEach(function($item, pos) {
                $item.data({position: pos});
            });
            self.selected = [stopPos];
            Ox.print('ids', self.ids);
        }

        function dropItem(id, pos) {
            var $item = self.$items[pos];
            $item.removeClass('OxDrag')
                .css({
                    cursor: 'pointer',
                    opacity: 1
                });
            that.triggerEvent('sort', {
                ids: self.ids
            });
            /*
            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];
            self.visibleColumns.splice(stopPos, 0, column);
            self.columnWidths.splice(stopPos, 0, width);
            that.$head.$content.empty();
            constructHead();
            Ox.print('s.vC', self.visibleColumns)
            $('.OxColumn' + Ox.toTitleCase(id)).css({
                opacity: 1
            });
            that.$titles[stopPos].removeClass('OxDrag').css({
                cursor: 'pointer'
            });
            that.$body.clearCache();
            */
        }

        function emptyFirstPage() {
            Ox.print('emptyFirstPage', self.$pages);
            self.$pages[0] && self.$pages[0].find('.OxEmpty').remove();
        }

        function fillFirstPage() {
            if (self.$pages[0]) {
                var height = getHeight(),
                    lastItemHeight = height % self.options.itemHeight || self.options.itemHeight,
                    visibleItems = Math.ceil(height / self.options.itemHeight);
                if (self.listLength < visibleItems) {
                    $.each(Ox.range(self.listLength, visibleItems), function(i, v) {
                        var $item = new Ox.ListItem({
                                construct: self.options.construct,
                            });
                        $item.addClass('OxEmpty').removeClass('OxTarget');
                        if (v == visibleItems - 1) {
                            $item.$element.css({
                                height: lastItemHeight + 'px',
                                overflowY: 'hidden'
                            });
                        }
                        $item.appendTo(self.$pages[0]);
                    });
                }
            }
        }

        function findCell(e) {
            var $element = $(e.target);
            while (!$element.hasClass('OxCell') && !$element.hasClass('OxPage') && !$element.is('body')) {
                $element = $element.parent();
            }
            return $element.hasClass('OxCell') ? $element : null;
        }

        function findItem(e) {
            Ox.print('---- findItem', e.target)
            var $element = $(e.target),
                $item = null;
            while (!$element.hasClass('OxTarget') && !$element.hasClass('OxPage') && !$element.is('body')) {
                $element = $element.parent();
            }
            if ($element.hasClass('OxTarget')) {
                while (!$element.hasClass('OxItem') && !$element.hasClass('OxPage') && !$element.is('body')) {
                    $element = $element.parent();
                }
                if ($element.hasClass('OxItem')) {
                    $item = $element;
                }
            }
            return $item;
        }

        function getAbove() {
            var pos = -1;
            if (self.selected.length) {
                pos = self.selected[self.selected.length - 1] - self.rowLength
                if (pos < 0) {
                    pos = -1;
                }
            }
            return pos;
        }

        function getBelow() {
            var pos = -1;
            if (self.selected.length) {
                pos = self.selected[self.selected.length - 1] + self.rowLength;
                if (pos >= self.$items.length) {
                    pos = -1;
                }
            }
            return pos;
        }

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

        function getListSize() {
            return Math.ceil(self.listLength *
                (self.options[self.options.orientation == 'horizontal' ?
                'itemWidth' : 'itemHeight'] + self.itemMargin) / self.rowLength);
        }

        function getNext() {
            var pos = -1;
            if (self.selected.length) {
                pos = (self.options.orientation == 'both' ?
                    self.selected[self.selected.length - 1] :
                    Ox.max(self.selected)) + 1;
                if (pos == self.$items.length) {
                    pos = -1;
                }
            }
            return pos;
        }

        function getPage() {
            return Math.max(
                Math.floor(self.options.orientation == 'horizontal' ?
                (that.scrollLeft() - self.listMargin / 2) / self.pageWidth :
                (that.scrollTop() - self.listMargin / 2) / self.pageHeight
            ), 0);
        }

        function getPageByPosition(pos) {
            return parseInt(pos / self.options.pageLength);
        }

        function getPageCSS(page) {
            return self.options.orientation == 'horizontal' ? {
                left: (page * self.pageWidth + self.listMargin / 2) + 'px',
                top: (self.listMargin / 2) + 'px',
                width: (page < self.pages - 1 ? self.pageWidth :
                    self.listLength % self.pageLength *
                    (self.options.itemWidth + self.itemMargin)) + 'px'
            } : {
                top: (page * self.pageHeight + self.listMargin / 2) + 'px',
                width: self.pageWidth + 'px'
            }
        }

        function getPageHeight() {
             return Math.ceil(self.pageLength * (self.options.itemHeight + self.itemMargin) / self.rowLength);
        }

        function getPosition() {
            // if orientation is both, this returns the
            // element position at the current scroll position
            return parseInt(
                that.scrollTop() / (self.options.itemHeight + self.itemMargin)
            ) * self.rowLength;
        }

        function getPositions(ids) {
            ids = ids || getSelectedIds();
            // fixme: optimize: send non-selected ids if more than half of the items are selected
            if (ids.length /*&& ids.length < self.listLength*/) {
                Ox.print('-------- request', {
                    ids: ids,
                    sort: self.options.sort
                });
                self.requests.push(self.options.request({
                    ids: ids,
                    sort: self.options.sort
                }, getPositionsCallback));
            } else {
                getPositionsCallback();
            }
        }

        function getPositionsCallback(result) {
            Ox.print('getPositionsCallback', result)
            var pos = 0;
            if (result) {
                $.extend(self, {
                    ids: {},
                    selected: []
                });
                $.each(result.data.positions, function(id, pos) {
                    Ox.print('id', id, 'pos', pos)
                    self.selected.push(pos);
                });
                pos = Ox.min(self.selected);
                self.page = getPageByPosition(pos);
            }
            // that.scrollTop(0);
            that.$content.empty();
            Ox.print('self.selected', self.selected, 'self.page', self.page);
            loadPages(self.page, function() {
                scrollToPosition(pos, true);
            });
        }

        function getPrevious() {
            var pos = -1;
            if (self.selected.length) {
                pos = (self.options.orientation == 'both' ?
                    self.selected[self.selected.length - 1] :
                    Ox.min(self.selected)) - 1;
            }
            return pos;
        }

        function getRow(pos) {
            return Math.floor(pos / self.rowLength);
        }

        function getRowLength() {
            return self.options.orientation == 'both' ?
                    Math.floor((getWidth() - self.listMargin) /
                    (self.options.itemWidth + self.itemMargin)) : 1
        }

        function getSelectedIds() {
            // fixme: is carring self.ids around the best way?
            return $.map(self.selected, function(pos) {
                return self.ids[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 loadPage(page, callback) {
            if (page < 0 || page >= self.pages) {
                !Ox.isUndefined(callback) && callback();
                return;
            }
            Ox.print('loadPage', page);
            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])) { // fixme: unload will have made this undefined already
                self.$pages[page] = constructEmptyPage(page);
                self.options.type == 'text' && page == 0 && fillFirstPage();
                self.$pages[page].appendTo(that.$content);
                self.requests.push(self.options.request({
                    keys: keys,
                    range: range,
                    sort: self.options.sort
                }, function(result) {
                    var $emptyPage = $.extend({}, self.$pages[page]);
                    self.$pages[page] = new Ox.ListPage().css(getPageCSS(page));
                    $.each(result.data.items, function(i, v) {
                        var pos = offset + i;
                        self.$items[pos] = new Ox.ListItem({
                            construct: self.options.construct,
                            data: v,
                            draggable: self.options.draggable,
                            format: self.options.format,
                            id: v[self.options.unique],
                            position: pos
                        });
                        self.ids[pos] = v[self.options.unique]; // fixme: why not use self.$items[pos].options('id')?
                        if (isSelected(pos)) {
                            Ox.print('pos', pos, 'isSelected')
                            self.$items[pos].addClass('OxSelected');
                        }
                        self.$items[pos].appendTo(self.$pages[page]);
                    });
                    self.options.type == 'text' && page == 0 && fillFirstPage();
                    $emptyPage.remove();
                    self.$pages[page].appendTo(that.$content);
                    !Ox.isUndefined(callback) && callback();
                }));
            } else {
                Ox.print('loading a page from cache, this should probably not happen -----------')
                self.$pages[page].appendTo(that.$content);
            }
        }

        function loadPages(page, callback) {
            var counter = 0,
                fn = function() {
                    Ox.print('---- self.$pages', self.$pages)
                    ++counter == 2 && !Ox.isUndefined(callback) && callback();
                };
            // fixme: find out which option is better
            loadPage(page, function() {
                //loadPage(page - 1, fn);
                //loadPage(page + 1, fn);
            });
            loadPage(page - 1, fn);
            loadPage(page + 1, fn);
        }

        function mousedown(e) {
            Ox.print('mousedown')
            var $cell,
                $item = findItem(e),
                pos,
                editTimeout = false;
                selectTimeout = false;
            that.gainFocus();
            if ($item) {
                if (!self.clickTimeout) {
                    // click
                    pos = $item.data('position');
                    if (e.metaKey) {
                        if (!isSelected(pos) && (self.options.max == -1 || self.options.max > self.selected.length)) {
                            addToSelection(pos);
                        } else if (isSelected(pos) && self.options.min < self.selected.length) {
                            deselect(pos);
                        }
                    } else if (e.shiftKey) {
                        if (self.options.max == -1) {
                            addAllToSelection(pos);
                        }
                    } else if (!isSelected(pos)) {
                        select(pos);
                    } else if (self.selected.length > 1) {
                        // this could be the first click
                        // of a double click on multiple items
                        selectTimeout = true;
                    } else if (self.options.type == 'text') {
                        $cell = findCell(e);
                        if ($cell && $cell.hasClass('OxEditable') && !$cell.hasClass('OxEdit')) {
                            if (self.options.sortable) {
                                editTimeout = true;
                            } else {
                                triggerEditEvent($item, $cell);
                            }
                        }
                    }
                    self.clickTimeout = setTimeout(function() {
                        self.clickTimeout = 0;
                        if (selectTimeout) {
                            select(pos);
                        }
                    }, 250);
                    if (self.options.sortable) {
                        self.dragTimeout = setTimeout(function() {
                            if (self.dragTimeout) {
                                dragItem(pos, e);
                                self.dragTimeout = 0;
                            }
                        }, 250);
                        $window.one('mouseup', function(e) {
                            if (self.dragTimeout) {
                                clearTimeout(self.dragTimeout);
                                self.dragTimeout = 0;
                                if (editTimeout) {
                                    triggerEditEvent($item, $cell);
                                }
                            }
                        });
                    }
                } else {
                    // dblclick
                    clearTimeout(self.clickTimeout);
                    self.clickTimeout = 0;
                    open();
                }
            } else if (self.options.min == 0) {
                selectNone();
            }
        }

        function open() {
            var ids = getSelectedIds();
            ids.length && that.triggerEvent('open', {
                ids: ids
            });
        }

        function preview() {
            var ids = getSelectedIds();
            if (ids.length) {
                self.preview = !self.preview;
                if (self.preview) {
                    that.triggerEvent('openpreview', {
                        ids: getSelectedIds()
                    });
                } else {
                    that.triggerEvent('closepreview');
                }
            }
        }

        function scroll() {
            var page = self.page;
            self.scrollTimeout && clearTimeout(self.scrollTimeout);
            self.scrollTimeout = setTimeout(function() {
                self.scrollTimeout = 0;
                self.page = getPage();
                if (self.page != page) {
                    Ox.print('page', page, '-->', self.page);
                }
                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);
                }
            }, 250);
        }

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

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

        function scrollTo(value) {
            that.animate(self.options.orientation == 'horizontal' ? {
                scrollLeft: (self.listSize * value) + 'px'
            } : {
                scrollTop: (self.listSize * value) + 'px'
            }, 0);
        }

        function scrollToFirst() {
            that[self.options.orientation == 'horizontal' ? 'scrollLeft' : 'scrollTop'](0);
        }

        function scrollToLast() {
            that[self.options.orientation == 'horizontal' ? 'scrollLeft' : 'scrollTop'](self.listSize);
        }

        function scrollToPosition(pos, leftOrTopAlign) {
            var itemHeight = self.options.itemHeight + self.itemMargin,
                itemWidth = self.options.itemWidth + self.itemMargin,
                positions = [],
                scroll,
                size;
            if (self.options.orientation == 'horizontal') {
                if (self.options.centered) {
                    that.animate({
                        scrollLeft: (self.listMargin / 2 + (pos + 0.5) * itemWidth - that.width() / 2) + 'px'
                    }, 0);
                } else {
                    positions[0] = pos * itemWidth + self.listMargin / 2;
                    positions[1] = positions[0] + itemWidth + self.itemMargin / 2;
                    scroll = that.scrollLeft();
                    size = getWidth();
                    if (positions[0] < scroll || leftOrTopAlign) {
                        that.animate({
                            scrollLeft: positions[0] + 'px'
                        }, 0);
                    } else if (positions[1] > scroll + size) {
                        that.animate({
                            scrollLeft: (positions[1] - size) + 'px'
                        }, 0);
                    }
                }
            } else {
                positions[0] = (self.options.orientation == 'vertical' ? pos : getRow(pos)) * itemHeight;
                positions[1] = positions[0] + itemHeight + (self.options.orientation == 'vertical' ? 0 : self.itemMargin);
                scroll = that.scrollTop();
                size = getHeight();
                if (positions[0] < scroll || leftOrTopAlign) {
                    that.animate({
                        scrollTop: positions[0] + 'px'
                    }, 0);
                } else if (positions[1] > scroll + size) {
                    that.animate({
                        scrollTop: (positions[1] - size) + 'px'
                    }, 0);
                }
            }
        }

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

        function selectAbove() {
            var pos = getAbove();
            if (pos > -1) {
                select(pos);
                scrollToPosition(pos);
            }
        }

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

        function selectBelow() {
            var pos = getBelow();
            if (pos > -1) {
                select(pos);
                scrollToPosition(pos);
            }
        }

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

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

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

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

        function setSelected(ids) {
            if (ids.length == 0) {
                // fixme: can't use selectNone here,
                // since it'd trigger a select event
                $.each(self.$items, function(pos) {
                    if (isSelected(pos)) {
                        self.selected.splice(self.selected.indexOf(pos), 1);
                        !Ox.isUndefined(self.$items[pos]) &&
                            self.$items[pos].removeClass('OxSelected');
                    }                    
                });
            }
            // fixme: implement the rest of this
            /*
            self.selected.forEach(function(pos) {
                if (value.indexOf(self.ids[pos]) == -1) {
                    deselect(pos);
                }
            });
            value.forEach(function(id) {
                if (self.ids.indexOf(id) > -1) {
                    addToSelection(pos)
                } else {
                    
                }
            });
            */
        }

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

        function triggerEditEvent($item, $cell) {
            that.triggerEvent('edit', {
                id: $item.data('id'),
                key: $cell.attr('class').split('OxColumn')[1].split(' ')[0].toLowerCase()
            });
        }

        function triggerSelectEvent() {
            var ids = self.options.selected = getSelectedIds();
            setTimeout(function() {
                var ids_ = getSelectedIds();
                Ox.print('ids', ids, 'ids after 100 msec', ids_)
                if (ids.length == ids_.length && (ids.length == 0 || ids[0] == ids_[0])) {
                    that.triggerEvent('select', {
                        ids: ids
                    });
                    self.preview && that.triggerEvent('openpreview', {
                        ids: ids
                    });
                } else {
                    Ox.print('select event not triggered after timeout');
                }
            }, 100);
        }

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

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

        function updateQuery(ids) {
            // ids are the selcected ids
            // (in case list is loaded with selection)
            clear();
            self.requests.push(self.options.request({}, function(result) {
                var keys = {};
                that.triggerEvent('load', result.data);
                self.rowLength = getRowLength();
                self.pageLength = self.options.orientation == 'both' ?
                        self.pageLengthByRowLength[self.rowLength] :
                        self.options.pageLength;
                $.extend(self, {
                    listLength: result.data.items,
                    pages: Math.ceil(result.data.items / self.pageLength),
                    pageWidth: self.options.orientation == 'vertical' ? 0 :
                            (self.options.itemWidth + self.itemMargin) *
                            (self.options.orientation == 'horizontal' ?
                            self.pageLength : self.rowLength),
                    pageHeight: self.options.orientation == 'horizontal' ? 0 :
                            Math.ceil(self.pageLength * (self.options.itemHeight +
                            self.itemMargin) / self.rowLength)
                });
                self.listSize = getListSize();
                that.$content.css(
                    self.options.orientation == 'horizontal' ? 'width' : 'height',
                    self.listSize + 'px'
                );
                getPositions(ids);
            }));
        }

        function updatePages(pos, scroll) {
            // only used if orientation is both
            clear();
            self.pageLength = self.pageLengthByRowLength[self.rowLength]
            $.extend(self, {
                listSize: getListSize(),
                pages: Math.ceil(self.listLength / self.pageLength),
                pageWidth: (self.options.itemWidth + self.itemMargin) * self.rowLength,
                pageHeight: getPageHeight()
            });
            that.$content.css({
                height: self.listSize + 'px'
            });
            self.page = getPageByPosition(pos);
            //that.scrollTop(0);
            that.$content.empty();
            loadPages(self.page, function() {
                scrollTo(scroll);
            });
        }

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

        self.onChange = function(key, value) {
            Ox.print('list onChange', key, value);
            if (key == 'request') {
                updateQuery();
            } else if (key == 'selected') {
                setSelected(value);
            }
        };

        that.clearCache = function() { // fixme: was used by TextList resizeColumn, now probably no longer necessary
            self.$pages = [];
            return that;
        };

        that.closePreview = function() {
            self.preview = false;
        };

        that.reload = function() {
            Ox.print('---------------- list reload, page', self.page)
            var page = self.page;
            clear();
            self.page = page
            that.$content.empty();
            loadPages(self.page);
            return that;
        };

        that.scrollToSelection = function() {
            self.selected.length && scrollToPosition(self.selected[0]);
        };

        that.size = function() { // fixme: not a good function name
            if (self.options.orientation == 'both') {
                var rowLength = getRowLength(),
                    pageLength = self.pageLengthByRowLength[rowLength],
                    pos = getPosition(),
                    scroll = that.scrollTop() / self.listSize;
                if (pageLength != self.pageLength) {
                    self.pageLength = pageLength;
                    self.rowLength = rowLength;
                    updatePages(pos, scroll);
                } else if (rowLength != self.rowLength) {
                    self.rowLength = rowLength;
                    self.pageWidth = (self.options.itemWidth + self.itemMargin) * self.rowLength; // fixme: make function
                    self.listSize = getListSize();
                    self.pageHeight = getPageHeight();
                    $.each(self.$pages, function(i, $page) {
                        !Ox.isUndefined($page) && $page.css({
                            width: self.pageWidth + 'px',
                            top: (i * self.pageHeight + self.listMargin / 2) + 'px'
                        });
                    });
                    that.$content.css({
                        height: self.listSize + 'px'
                    });
                    Ox.print('scrolling to', scroll)
                    scrollTo(scroll);
                }
            } else if (self.options.type == 'text') {
                Ox.print('that.size, type==text')
                emptyFirstPage();
                fillFirstPage();
            }
        }

        that.sortList = 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;
        }

        return that;

    };

    Ox.ListItem = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    construct: function() {},
                    data: {},
                    draggable: false,
                    format: [],
                    id: '',
                    position: 0
                })
                .options(options || {});

        // make a clone, so we don't format cached data
        self.data = $.extend({}, self.options.data);

        $.each(self.data, function(k, v) {
            var format = self.options.format[k];
            if (Ox.isArray(v)) {
                self.data[k] = v.join(', ');
            } else if (format) {
                self.data[k] = Ox.isObject(format) ? 
                    Ox['format' + Ox.toTitleCase(format.type)]
                    .apply(this, $.merge([v], format.args)) :
                    format(v);
            }
        });

        that.$element = self.options.construct(self.data)
            .addClass('OxItem')
            .attr({
                draggable: self.options.draggable
                //id: self.options.id.replace(/./g, '_') // fixme: dots are not the only problem
            })
            .data({
                id: self.options.id,
                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: [],
                    columnsMovable: false,
                    columnsRemovable: false,
                    columnsResizable: false,
                    columnsVisible: false,
                    columnWidth: [40, 800],
                    id: '',
                    max: -1,
                    min: 0,
                    pageLength: 100,
                    selected: [],
                    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?
            // fixme: and can't these just remain undefined?
            if (Ox.isUndefined(v.editable)) {
                v.editable = false;
            }
            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: [],
            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, {
            columnWidths: $.map(self.visibleColumns, function(v, i) {
                return v.width;
            }),
            pageHeight: self.options.pageLength * self.itemHeight
        });

        self.format = {};
        self.options.columns.forEach(function(v, i) {
            if (v.format) {
                self.format[v.id] = v.format;
            }
        });

        // Head

        if (self.options.columnsVisible) {
            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');
            constructHead();
            if (self.options.columnsRemovable) {
                that.$select = new Ox.Select({
                        id: self.options.id + 'SelectColumns',
                        items: $.map(self.options.columns, function(v, i) {
                            return {
                                checked: v.visible,
                                disabled: v.removable === false,
                                id: v.id,
                                title: v.title
                            }
                        }),
                        max: -1,
                        min: 1,
                        type: 'image'
                    })
                    .bindEvent('change', changeColumns)
                    .appendTo(that.$bar.$element);
            }
        }

        // Body

        that.$body = new Ox.List({
                construct: constructItem,
                id: self.options.id,
                itemHeight: 16,
                itemWidth: getItemWidth(),
                format: self.format,
                keys: $.map(self.visibleColumns, function(v) {
                    return v.id;
                }),
                max: self.options.max,
                min: self.options.min,
                pageLength: self.options.pageLength,
                orientation: 'vertical',
                request: self.options.request,
                selected: self.options.selected,
                sort: self.options.sort,
                sortable: self.options.sortable,
                type: 'text',
                unique: self.unique
            }, $.extend({}, self)) // pass event handler
            .addClass('OxBody')
            .css({
                top: (self.options.columnsVisible ? 16 : 0) + 'px',
                overflowY: (self.options.columnsVisible ? 'scroll' : 'auto')
            })
            .scroll(function() {
                var scrollLeft = $(this).scrollLeft();
                if (scrollLeft != self.scrollLeft) {
                    self.scrollLeft = scrollLeft;
                    that.$head && that.$head.scrollLeft(scrollLeft);
                }
            })
            .bindEvent({
                edit: function(event, data) {
                    editCell(data.id, data.key, data.item, data.cell);
                },
                select: function(event, data) {
                    self.options.selected = data.ids;
                }
            })
            .appendTo(that);
        that.$body.$content.css({
            width: getItemWidth() + 'px'
        });

        Ox.print('s.vC', self.visibleColumns)

        function addColumn(id) {
            Ox.print('addColumn', id);
            var column,
                index = 0;
            $.each(self.options.columns, function(i, v) {
                if (v.visible) {
                    index++;
                } else if (v.id == id) {
                    column = v;
                    return false;
                }
            });
            column.visible = true;
            self.visibleColumns.splice(index, 0, column);
            self.columnWidths.splice(index, 0, column.width);
            that.$head.$content.empty();
            constructHead();
            that.$body.options({
                keys: $.map(self.visibleColumns, function(v, i) {
                    return v.id;
                })
            });
            that.$body.reload();
        }

        function changeColumns(event, data) {
            var add,
                ids = [];
            $.each(data.selected, function(i, column) {
                var index = getColumnIndexById(column.id);
                if (!self.options.columns[index].visible) {
                    addColumn(column.id);
                    add = true;
                    return false;
                }
                ids.push(column.id);
            });
            if (!add) {
                $.each(self.visibleColumns, function(i, column) {
                    if (ids.indexOf(column.id) == -1) {
                        removeColumn(column.id);
                        return false;
                    }
                });
            }
        }

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

        function constructHead() {
            var offset = 0;
            that.$titles = [];
            self.columnOffsets = [];
            $.each(self.visibleColumns, function(i, v) {
                var $order, $resize, $left, $center, $right, timeout = 0;
                offset += self.columnWidths[i];
                self.columnOffsets[i] = offset - self.columnWidths[i] / 2;
                that.$titles[i] = $('<div>')
                    .addClass('OxTitle OxColumn' + Ox.toTitleCase(v.id))
                    .css({
                        width: (self.columnWidths[i] - 9) + 'px',
                        textAlign: v.align
                    })
                    .html(v.title)
                    .mousedown(function(e) {
                        timeout = setTimeout(function() {
                            self.options.columnsMovable && dragColumn(v.id, e);
                            timeout = 0;
                        }, 250);
                    })
                    .mouseup(function() {
                        if (timeout) {
                            clearTimeout(timeout);
                            timeout = 0;
                            clickColumn(v.id);
                        }
                    })
                    .appendTo(that.$head.$content.$element);
                $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')
                    .appendTo(that.$head.$content.$element);
                if (self.options.columnsResizable) {
                    $resize.addClass('OxResizable')
                        .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.one('mouseup', function() {
                                $window.unbind('mousemove');
                            });
                        })
                        .dblclick(function() {
                            resizeColumn(v.id, v.width);
                        });
                }
                $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'
            });
            Ox.print('s.sC', self.selectedColumn)
            Ox.print('s.cO', self.columnOffsets)
            if (getColumnPositionById(self.options.columns[self.selectedColumn].id) > -1) { // fixme: save in var
                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'
                });
            }
        }

        function constructItem(data) {
            var $item = $('<div>')
                    .addClass('OxTarget')
                    .css({
                        width: getItemWidth() + 'px'
                    });
            $.each(self.visibleColumns, function(i, v) {
                var $cell = $('<div>')
                        .addClass(
                            'OxCell OxColumn' + Ox.toTitleCase(v.id) +
                            (v.editable ? ' OxEditable' : '')
                        )
                        .css({
                            width: (self.columnWidths[i] - (self.options.columnsVisible ? 9 : 8)) + 'px',
                            borderRightWidth: (self.options.columnsVisible ? 1 : 0) + 'px',
                            textAlign: v.align
                        })
                        .html(v.id in data ? data[v.id] : '')
                        .appendTo($item);
            });
            return $item;
        }

        function dragColumn(id, e) {
            var startX = e.clientX,
                startPos = getColumnPositionById(id),
                pos = startPos,
                stopPos = startPos,
                offsets = $.map(self.visibleColumns, function(v, i) {
                    return self.columnOffsets[i] - self.columnOffsets[startPos]
                });
            $('.OxColumn' + Ox.toTitleCase(id)).css({
                opacity: 0.25
            });
            that.$titles[startPos].addClass('OxDrag').css({ // fixme: why does the class not work?
                cursor: 'move'
            });
            Ox.print('offsets', offsets)
            $window.mousemove(function(e) {
                var d = e.clientX - startX;
                $.each(offsets, 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.one('mouseup', function() {
                dropColumn(id, pos);
                $window.unbind('mousemove');
            });
        }

        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];
            self.visibleColumns.splice(stopPos, 0, column);
            self.columnWidths.splice(stopPos, 0, width);
            that.$head.$content.empty();
            constructHead();
            Ox.print('s.vC', self.visibleColumns)
            $('.OxColumn' + Ox.toTitleCase(id)).css({
                opacity: 1
            });
            that.$titles[stopPos].removeClass('OxDrag').css({
                cursor: 'pointer'
            });
            that.$body.clearCache();
        }

        function editCell(id, key) {
            Ox.print('editCell')
            var $item = getItem(id),
                $cell = getCell(id, key),
                $input,
                html = $cell.html(),
                index = getColumnIndexById(key),
                column = self.options.columns[index],
                width = column.width;
            $cell.empty()
                .addClass('OxEdit')
                .css({
                    width: width + 'px'
                });
            $input = Ox.Input({
                    autovalidate: column.input.autovalidate,
                    style: 'square',
                    value: html,
                    width: width
                })
                .bind({
                    mousedown: function(e) {
                        // keep mousedown from reaching list
                        e.stopPropagation();
                    }
                })
                .bindEvent({
                    blur: submit,
                })
                .appendTo($cell)
                .focus();
            function submit() {
                var value = $input.value();
                //$input.loseFocus().remove();
                // fixme: leaky, inputs remain in focus stack
                $cell.removeClass('OxEdit')
                    .css({
                        width: (width - 8) + 'px'
                    })
                    .html(value)
                that.triggerEvent('submit', {
                    id: id,
                    key: key,
                    value: value
                });
            }
        }

        function getCell(id, key) {
            var $item = getItem(id);
            return $($item.find('.OxCell.OxColumn' + Ox.toTitleCase(key))[0]);
        }

        function getColumnIndexById(id) {
            return Ox.getPositionById(self.options.columns, id);
        }

        function getColumnPositionById(id) {
            return Ox.getPositionById(self.visibleColumns, id);
        }

        function getItem(id) {
            var $item = null;
            $.each(that.find('.OxItem'), function(i, v) {
                $v = $(v);
                if ($v.data('id') == id) {
                    $item = $v;
                    return false;
                }
            })
            return $item;
        }

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

        function moveColumn(id, pos) {
            // fixme: column head should be one element, not three
            Ox.print('moveColumn', id, pos)
            var startPos = getColumnPositionById(id),
                stopPos = pos,
                startClassName = '.OxColumn' + Ox.toTitleCase(id),
                stopClassName = '.OxColumn' + Ox.toTitleCase(self.visibleColumns[stopPos].id),
                insert = startPos < stopPos ? 'insertAfter' : 'insertBefore'
                $column = $('.OxTitle' + startClassName),
                $order = $column.next(),
                $resize = $order.next();
            Ox.print(startClassName, insert, stopClassName)
            $column.detach()[insert](insert == 'insertAfter' ? $('.OxTitle' + stopClassName).next().next() : $('.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()[insert]($v.children(stopClassName));
            });
            var column = self.visibleColumns.splice(startPos, 1)[0],
                width = self.columnWidths.splice(startPos, 1)[0];
            self.visibleColumns.splice(stopPos, 0, column);
            self.columnWidths.splice(stopPos, 0, width);
        }

        function removeColumn(id) {
            Ox.print('removeColumn', id);
            var className = '.OxColumn' + Ox.toTitleCase(id),
                index = getColumnIndexById(id),
                itemWidth,
                position = getColumnPositionById(id),
                $column = $('.OxTitle' + className),
                $order = $column.next(),
                $resize = $order.next();
            self.options.columns[index].visible = false;
            self.visibleColumns.splice(position, 1);
            self.columnWidths.splice(position, 1);
            that.$head.$content.empty();
            constructHead();
            itemWidth = getItemWidth();
            $.each(that.$body.find('.OxItem'), function(i, v) {
                var $v = $(v);
                $v.children(className).remove();
                $v.css({
                    width: itemWidth + 'px'
                });
            });
            that.$body.$content.css({
                width: itemWidth + 'px'
            });
            that.$body.options({
                keys: $.map(self.visibleColumns, function(v, i) {
                    return v.id;
                })
            });
            //that.$body.clearCache();
        }

        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'
            });
            $('.OxCell.OxColumn' + Ox.toTitleCase(self.options.columns[i].id)).css({
                width: (width - 9) + 'px'
            });
            setWidth();
            //that.$body.clearCache();
        }

        function setWidth() {
            var width = getItemWidth();
            that.$body.$content.find('.OxItem').css({ // fixme: can we avoid this lookup?
                width: width + 'px'
            });
            that.$body.$content.css({
                width: width + 'px' // fixme: check if scrollbar visible, and listen to resize/toggle event
            });
        }

        function toggleSelected(id) {
            var pos = getColumnPositionById(id);
            if (pos > -1) {
                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') {
                //alert('request set!!')
                that.$body.options(key, value);
            } else if (key == 'selected') {
                that.$body.options(key, value);
            }
        };

        that.closePreview = function() {
            that.$body.closePreview();
        };

        that.resizeColumn = function(id, width) {
            resizeColumn(id, width);
            return that;
        }

        that.size = function() {
            setWidth();
            that.$body.size();
        }

        that.sortList = function(key, operator) {
            var isSelected = key == self.options.sort[0].key;
            self.options.sort = [
                {
                    key: key,
                    operator: operator
                }
            ];
            if (self.options.columnsVisible) {
                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.sortList(self.options.sort[0].key, self.options.sort[0].operator);
            return that;
        };

        that.value = function(id, key, value) {
            if (Ox.isUndefined(value)) {
                
            } else {
                return that;
            }
        }

        return that;

    };

    /*
    ============================================================================
    Maps
    ============================================================================
    */

    Ox.Map = function(options, self) {

        var self = self || {}
            that = new Ox.Element('div', self)
                .defaults({
					clickable: false,
                    places: [],
                    type: 'satellite'
                })
                .options(options || {})
				.bindEvent({
                    key_up: function() {
						pan(0, -1);
					},
                    key_down: function() {
						pan(0, 1);
					},
                    key_left: function() {
						pan(-1, 0);
					},
                    key_right: function() {
						pan(1, 0);
					},
					key_0: reset,
					key_minus: function() {
						zoom(-1);
					},
					key_equal: function() {
						zoom(1);
					},
                    key_enter: focusOnPlace,
					key_shift_enter: zoomToPlace,
                    key_escape: deselectPlace
                });

        if (Ox.isUndefined(window.google)) {
            window.google = function() {};
            $.getScript('http://maps.google.com/maps/api/js?callback=google&sensor=false', function() {
                var interval = setInterval(function() {
                    if (!Ox.isUndefined(google.maps)) {
                        clearInterval(interval);
                        initMap();
                    }
                }, 100);
            });
        } else {
            initMap();
        }

        function initMap() {
            $.extend(self, {
                geocoder: new google.maps.Geocoder(),
                selected: -1
            });
    		if (Ox.isObject(self.options.places[0])) {
    			$.each(self.options.places, function(i, place) {
    				place.bounds = getBounds(),
    				place.center = place.bounds.getCenter();
    				$.extend(place, {
    					marker: new Marker(place),
    					polygon: new Polygon(place)
    				});
    				self.bounds = i == 0 ? getBounds() : self.bounds.union(place.bounds);
    				self.options.places[i] = place;
    				function getBounds() {
    					return new google.maps.LatLngBounds(
    						new google.maps.LatLng(place.points.southwest[0], place.points.southwest[1]),
    						new google.maps.LatLng(place.points.northeast[0], place.points.northeast[1])
    					);
    				}
    	        });
    	       	loadMap();
    		} else {
    			self.counter = 0;
    			$.each(self.options.places, function(i, place) {
    				getLocationByName(place, function(place) {
    					Ox.print(self.counter, i, place);
    					self.options.places[i] = place;
    					if (self.counter++ == self.options.places.length - 1) {
    						loadMap();
    					}
    				});
    			});
    		}
        }

		function loadMap() {
			self.center = self.bounds ? self.bounds.getCenter() : new google.maps.LatLng(0, 0);
			self.zoom = 1;
	        $.extend(self, {
	            map: new google.maps.Map(that.$element[0], {
	                center: self.center,
	                disableDefaultUI: true,
	                mapTypeId: google.maps.MapTypeId[self.options.type.toUpperCase()],
	                zoom: self.zoom
	            })
	        });
			if (self.bounds) {
		        self.map.fitBounds(self.bounds);
				// self.center = self.map.getCenter();
				self.zoom = self.map.getZoom();				
			}
	        google.maps.event.addListener(self.map, 'click', click);
	        google.maps.event.addListener(self.map, 'zoom_changed', zoomChanged);
	        $.each(self.options.places, function(i, place) {
	            place.marker.add();
	        });
	        resize();
			that.gainFocus();
			that.triggerEvent('loaded');
		}

        function canContain(outerBounds, innerBounds) {
            var outerSpan = outerBounds.toSpan(),
                innerSpan = innerBounds.toSpan();
            return outerSpan.lat() > innerSpan.lat() &&
                    outerSpan.lng() > innerSpan.lng();
        }

        function click(event) {
            Ox.print('event', event);
			that.gainFocus();
			if (self.options.clickable) {
	            getLocationByLatLng(event.latLng, self.map.getBounds(), function(location) {
	                self.marker && self.marker.remove();
	                self.polygon && self.polygon.remove();
	                if (location) {
	                    self.marker = location.marker.add();
	                    self.polygon && self.polygon.remove();
	                    self.polygon = location.polygon.add();
	                    that.triggerEvent('select', location);
	                }
	            });
			}
        }

		function deselectPlace() {
			
		}

		function focusOnPlace() {
			if (self.selected > -1) {
				self.map.panTo(self.options.places[self.selected].center);
			}
		}

        function getLocationByLatLng(latlng, bounds, callback) {
            Ox.print('ll b', latlng, bounds)
            var callback = arguments.length == 3 ? callback : bounds,
                bounds = arguments.length == 3 ? bounds : null;
            self.geocoder.geocode({
                latLng: latlng
            }, function(results, status) {
                Ox.print('results', results)
                var length = results.length;
                if (status == google.maps.GeocoderStatus.OK) {
                    if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {
                        if (bounds) {
                            $.each(results.reverse(), function(i, result) {
                                if (
                                    i == length - 1 ||
                                    canContain(bounds, result.geometry.bounds || result.geometry.viewport)
                                ) {
                                    callback(new Location(results[i]));
                                    return false;
                                }
                            });
                        } else {
                            callback(new Location(results[0]));
                        }
                    } else {
                        callback(null);
                    }
                } else {
                    Ox.print('geocode failed:', status);
                    callback(null);
                }
            });
        }

        function getLocationByName(name, callback) {
            self.geocoder.geocode({
                address: name
            }, function(results, status) {
                if (status == google.maps.GeocoderStatus.OK) {
                    if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {
                        callback(new Location(results[0]))
                    } else {
                        callback(null);
                    }
                } else {
                    Ox.print('geocode failed:', status);
                    callback(null);
                }
            });
        }

		function getPositionByName(name) {
			var position = -1;
			$.each(self.options.places, function(i, place) {
				if (place.name == name) {
					position = i;
					return false;
				}
			});
			return position;
		}

		function pan(x, y) {
			self.map.panBy(x * 256, y * 256);
		};

        function resize() {
            google.maps.event.trigger(self.map, 'resize');
        }

		function reset() {
			Ox.print(self.map.getZoom(), self.zoom);
			self.map.getZoom() == self.zoom ?
					self.map.panTo(self.center) :
					self.map.fitBounds(self.bounds);
		}

        function zoomChanged() {
            that.triggerEvent('zoom', {
                value: self.map.getZoom()
            });
        }

		function zoom(z) {
			self.map.setZoom(self.map.getZoom() + z);
		}

		function zoomToPlace() {
			if (self.selected > -1) {
				self.map.fitBounds(self.options.places[self.selected].bounds);
			}
		}

        function Location(geodata) {
            Ox.print('geodata', geodata);
            var bounds = geodata.geometry.bounds || geodata.geometry.viewport,
				center = bounds.getCenter(),
				location = {
					bounds: bounds,
					center: center,
					geoname: geodata.formatted_address,
					points: {
						'center': [center.lat(), center.lng()],
						'southwest': [bounds.getSouthWest().lat(), bounds.getSouthWest().lng()],
						'northeast': [bounds.getNorthEast().lat(), bounds.getNorthEast().lng()]
					},
					name: geodata.formatted_address.split(', ')[0],
	 				size: 0,
					type: geodata.address_components[0].types.join(', ')
				};
            return $.extend(location, {
                marker: new Marker(location),
                polygon: new Polygon(location)
            });
        }

        function Marker(place) {
			Ox.print(place.center)
            var listeners = {},
                marker = new google.maps.Marker({
                    icon: icon('red'),
                    position: place.center,
                    title: place.name
                }),
                selected = false;
            function click() {
				Ox.print('click', self.selected, selected)
                selected = !selected;
				selected && self.selected > -1 && self.options.places[self.selected].marker.deselect();
				self.selected = selected ? getPositionByName(place.name) : -1;
                marker.setOptions({
                    icon: icon(selected ? 'blue' : 'red')
                });
                place.polygon[selected ? 'add' : 'remove']();
				that.triggerEvent(selected ? 'select' : 'deselect', place);
            }
			function deselect() {
				
			}
            function dblclick() {
				Ox.print('dblclick', place.bounds)
                self.map.fitBounds(place.bounds);
            }
			function select() {
				
			}
            function icon(color) {
                return oxui.path + 'png/ox.ui/marker' + Ox.toTitleCase(color) + '.png'
            }
            return {
                add: function() {
					Ox.print('add Marker')
                    marker.setMap(self.map);
                    listeners = {
                        click: google.maps.event.addListener(marker, 'click', click),
                        dblclick: google.maps.event.addListener(marker, 'dblclick', dblclick),
                    };
                    return this;
                },
                deselect: function() {
					self.selected = -1;
					selected = false;
					marker.setOptions({
						icon: icon('red')
					});
                    place.polygon.remove();
                },
                remove: function() {
                    marker.setMap(null);
                    $.each(listeners, function(i, listener) {
                        google.maps.event.removeListener(listener);
                    });
                    return this;
                },
                select: function() {
					if (self.selected > -1) {
						self.options.places[self.selected].marker.deselect();
					}
					self.selected = getPositionByName(place.name);
                    selected = true;
					marker.setOptions({
						icon: icon('blue')
					});
					place.polygon.add();
                }
            };
        }

        function Polygon(location) {
            var listeners = {},
                paths = [
                    new google.maps.LatLng(location.points.southwest[0], location.points.southwest[1]),
                    new google.maps.LatLng(location.points.northeast[0], location.points.southwest[1]),
                    new google.maps.LatLng(location.points.northeast[0], location.points.northeast[1]),
                    new google.maps.LatLng(location.points.southwest[0], location.points.northeast[1]),
                    new google.maps.LatLng(location.points.southwest[0], location.points.southwest[1])
                ],
                polygon = new google.maps.Polygon({
                    paths: paths
                }),
                selected = false;
            setOptions();
            function click() {
                selected = !selected;
                setOptions();
            }
            function setOptions() {
                var color = selected ? '#8080FF' : '#FFFFFF';
                polygon.setOptions({
                    clickable: true,
                    fillColor: color,
                    fillOpacity: selected ? 0.1 : 0,
                    strokeColor: color,
                    strokeOpacity: 1,
                    strokeWeight: 2
                });
            }
            return {
                add: function() {
                    polygon.setMap(self.map);
                    listeners.click = google.maps.event.addListener(polygon, 'click', click);
                    return this;
                },
                deselect: function() {
                    selected = false;
                    setOptions();
                },
                remove: function() {
                    polygon.setMap(null);
                    google.maps.event.removeListener(listeners.click);
                    return this;
                },
                select: function() {
                    selected = true;
                    setOptions();
                }
            };
        }

        function Rectangle(area) { // fixme: not used
            var latlng = {
                    sw: new google.maps.LatLng(area[0][0], area[0][1]),
                    ne: new google.maps.LatLng(area[1][0], area[1][1])
                },
                bounds = new google.maps.LatLngBounds(latlng.sw, latlng.ne),
                lat = {},
                lng = {};
            latlng.mc = bounds.getCenter();
            $.each(latlng, function(k, v) {
                lat[k] = v.lat();
                lng[k] = v.lng();
            });
            $.extend(latlng, {
                sc: new google.maps.LatLng(lat.sw, lng.mc),
                se: new google.maps.LatLng(lat.sw, lng.ne),
                mw: new google.maps.LatLng(lat.mc, lng.sw),
                me: new google.maps.LatLng(lat.mc, lng.ne),
                nw: new google.maps.LatLng(lat.ne, lng.sw),
                nc: new google.maps.LatLng(lat.ne, lng.mc),
            });
            return {
                area: area,
                bounds: bounds,
                canContain: function(rectangle) {
                    var outerSpan = this.bounds.toSpan(),
                        innerSpan = rectangle.bounds.toSpan();
                    return outerSpan.lat() > innerSpan.lat() &&
                            outerSpan.lng() > innerSpan.lng();
                },
                center: latlng.mc,
                contains: function(rectangle) {
                    return this.bounds.contains(rectangle.bounds.getSouthWest()) &&
                            this.bounds.contains(rectangle.bounds.getNorthEast());
                },
                latlng: latlng
            };
        }

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

        that.find = function(name, callback) {
            getLocationByName(name, function(place) {
                if (place) {
                    //self.marker = location.marker.add();
                    self.polygon && self.polygon.remove();
                    self.polygon = place.polygon.add();
                    self.bounds = place.bounds;
                    self.map.fitBounds(self.bounds);
                }
                callback(place);
            });
        };

        that.triggerResize = function() {
            Ox.print('---- triggerResize');
            var center = self.map.getCenter();
            google.maps.event.trigger(self.map, 'resize');
            self.map.setCenter(center);
        }

        that.zoom = function(value) {
            self.map.setZoom(value);
        };

        return that;

    };


    /**
        options
            height      image height (px)
            places      array of either names (''), points ([0, 0]),
                        or objects ({name, point, highlight})
            type        map type ('hybrid', 'roadmap', 'satellite', 'terrain')
            width       image width (px)
    */
    Ox.MapImage = function(options, self) {


        var self = self || {},
            that = new Ox.Element('img', self)
                .defaults({
                    height: 360,
                    markerColorHighlight: 'yellow',
                    markerColorNormal: 'blue',
                    places: [],
                    type: 'satellite',
                    width: 640
                })
                .options(options || {})

        $.extend(self, {
            markers: {
                highlight: [],
                normal: []
            },
            src: 'http://maps.google.com/maps/api/staticmap?sensor=false' +
                    '&size=' + self.options.width + 'x' + self.options.height +
                    '&maptype=' + self.options.type
        });

        if (self.options.places.length) {
            $.each(self.options.places, function(i, place) {
                if (Ox.isString(place)) {
                    self.markers.normal.push(place);
                } else if (Ox.isArray(place)) {
                    self.markers.normal.push(place.join(','));
                } else {
                    self.markers[place.highlight ? 'highlight' : 'normal']
                        .push('point' in place ? place.point.join(',') : place.name)
                }
            });
            $.each(self.markers, function(k, markers) {
                if (markers.length) {
                    self.src += '&markers=icon:' + 'http://dev.pan.do:8000' + oxui.path + 'png/ox.ui/marker' +
                            Ox.toTitleCase(self.options['markerColor' + Ox.toTitleCase(k)]) + '.png|' +
                            markers.join('|')
                }
            });
        } else {
            self.src += '&center=0,0&zoom=2'
        }

        that.attr({
            src: self.src
        });

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

        return that;

    };

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

    /**
    */
    Ox.MainMenu = function(options, self) {
        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
                }))
                .bindEvent({
                    hide: 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.checkItem = function(id) {
            var ids = id.split('_'),
                itemId = ids.pop(),
                menuId = ids.join('_');
            that.getMenu(menuId).checkItem(itemId);
        };

        that.disableItem = function(id) {
            that.getItem(id).options({
                disabled: true
            });
        };

        that.enableItem = function(id) {
            that.getItem(id).options({
                disabled: false
            });
        };

        that.getItem = function(id) {
            var ids = id.split('_'),
                item;
            if (ids.length == 1) {
                $.each(that.menus, function(i, menu) {
                    item = menu.getItem(id);
                    return !item;
                });
            } else {
                item = that.getMenu(ids.shift()).getItem(ids.join('_'));
            }
            Ox.print('getItem', id, item);
            return item;
        };

        that.getMenu = function(id) {
            var ids = id.split('_'),
                menu;
            if (ids.length == 1) {
                $.each(that.menus, function(i, v) {
                    if (v.options('id') == id) {
                        menu = v;
                        return false;
                    }
                });
            } else {
                menu = that.getMenu(ids.shift()).getSubmenu(ids.join('_'));
            }
            Ox.print('getMenu', id, menu);
            return menu;
        };

        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);
            }
        };

        that.uncheckItem = function(id) {
            that.getItem(id).options({
                checked: false
            });
        };

        return that;

    };

    /**
        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
    */
    Ox.Menu = function(options, self) {
        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)
                .bindEvent({
                    key_up: selectPreviousItem,
                    key_down: selectNextItem,
                    key_left: selectSupermenu,
                    key_right: selectSubmenu,
                    key_escape: hideMenu,
                    key_enter: clickSelectedItem
                }),
            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' : 'OxMenuLayer')
            .click(click);

        function click(event) {
            var item,
                position,
                $target = $(event.target),
                $parent = $target.parent();
            // necessary for highlight
            if ($parent.is('.OxCell')) {
                $target = $parent;
                $parent = $target.parent();
            }
            if ($target.is('.OxCell')) {
                position = $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],
                toggled;
            that.hideMenu();
            if (!item.options('items').length) {
                if (that.options('parent')) {
                    that.options('parent').hideMenu().triggerEvent('click');
                }
                if (item.options('checked') !== null) {
                    if (item.options('group')) {
                        Ox.print('has group', item.options('group'))
                        toggled = self.optionGroups[item.options('group')].toggle(position);
                        Ox.print('toggled', toggled)
                        if (toggled.length) {
                            $.each(toggled, function(i, pos) {
                                that.items[pos].toggleChecked();
                            });
                            Ox.print('--triggering change event--');
                            (self.options.mainmenu || self.options.parent || that).triggerEvent('change', {
                                id: item.options('group'),
                                checked: $.map(self.optionGroups[item.options('group')].checked(), function(v, i) {
                                    return {
                                        id: that.items[v].options('id'),
                                        title: Ox.stripTags(that.items[v].options('title')[0])
                                    };
                                })
                            });
                        }
                    } else {
                        item.toggleChecked();
                        (self.options.mainmenu || that).triggerEvent('change', {
                            checked: item.options('checked'),
                            id: item.options('id'),
                            title: Ox.stripTags(item.options('title')[0])
                        });
                    }
                } else {
                    (self.options.mainmenu || that).triggerEvent('click', {
                        id: item.options('id'),
                        title: Ox.stripTags(item.options('title')[0])
                    });
                }
                if (item.options('title').length == 2) {
                    item.toggleTitle();
                }
            }
        }

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

        function constructItems(items) {

            that.$content.empty();
            scrollMenuUp();

            self.optionGroups = {};
            $.each(items, function(i, item) {
                if (item.group) {
                    items[i] = $.map(item.items, function(v, i) {
                        return $.extend(v, {
                            group: item.group
                        });
                    });
                    self.optionGroups[item.group] = new Ox.OptionGroup(
                        items[i],
                        'min' in item ? item.min : 1,
                        'max' in item ? item.max : 1
                    );
                }
            });
            items = Ox.flatten(items);

            that.items = [];
            $.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 getItemPositionById(id) {
            var position;
            $.each(that.items, function(i, v) {
                if (v.options('id') == id) {
                    position = i;
                    return false;
                }
            });
            return position;
        }

        function hideMenu() {
            // called on key_escape
            that.hideMenu();
        }

        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);
                $parent = $target.parent();
            if ($parent.is('.OxCell')) {
                $target = $parent;
                $parent = $target.parent();
            }
            if ($target.is('.OxCell')) {
                position = $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) {
                //Ox.print('s.o.s', self.options.selected, that.items)
                item = that.items[self.options.selected]
                item.removeClass('OxSelected');
                /* disabled
                that.triggerEvent('deselect', {
                    id: item.options('id'),
                    title: Ox.stripTags(item.options('title')[0])
                });
                */
            }
            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');
                /* disabled
                that.triggerEvent('select', {
                    id: item.options('id'),
                    title: Ox.stripTags(item.options('title')[0])
                });
                */
            }
            self.options.selected = position;
        }

        function selectNextItem() {
            var offset,
                selected = self.options.selected;
            Ox.print('sNI', 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;
            Ox.print('sPI', 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() {
            Ox.print('selectSubmenu', self.options.selected)
            if (self.options.selected > -1) {
                var submenu = that.submenus[that.items[self.options.selected].options('id')];
                Ox.print('submenu', submenu, that.submenus);
                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() {
            Ox.print('selectSupermenu', self.options.selected)
            if (self.options.parent) {
                self.options.selected > -1 && that.items[self.options.selected].trigger('mouseleave');
                scrollMenuUp();
                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') {
                that.$content.find('.OxSelected').removeClass('OxSelected');
                selectItem(value);
            }
        }

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

        that.addItemAfter = function(item, id) {

        };

        that.addItemBefore = function(item, id) {

        };

        that.checkItem = function(id) {
            var item = that.getItem(id);
            if (item.options('group')) {
                var position = getItemPositionById(id),
                    toggled = self.optionGroups[item.options('group')].toggle(position);
                if (toggled.length) {
                    $.each(toggled, function(i, pos) {
                        that.items[pos].toggleChecked();
                    });
                }
            } else {
                item.options({
                    checked: true
                });
            }
        };

        that.getItem = function(id) {
            //Ox.print('id', id)
            var ids = id.split('_'),
                item;
            if (ids.length == 1) {
                $.each(that.items, function(i, v) {
                    if (v.options('id') == id) {
                        item = v;
                        return false;
                    }
                });
                if (!item) {
                    $.each(that.submenus, function(k, submenu) {
                        item = submenu.getItem(id);
                        return !item;
                    });
                }
            } else {
                item = that.submenus[ids.shift()].getItem(ids.join('_'));
            }
            return item;
        };

        that.getSubmenu = function(id) {
            var ids = id.split('_'),
                submenu;
            if (ids.length == 1) {
                submenu = that.submenus[id];
            } else {
                submenu = that.submenus[ids.shift()].getSubmenu(ids.join('_'));
            }
            Ox.print('getSubmenu', id, submenu);
            return submenu;
        }

        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();
            if (self.options.parent) {
                //self.options.element.removeClass('OxSelected');
                self.options.parent.options({
                    selected: -1
                });
            }
            that.hide()
                .loseFocus()
                .triggerEvent('hide');
            that.$layer.hide();
            return that;
        };

        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 == 0 && 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 = Ox.limit(
                    offset.left + self.options.offset.left + (self.options.side == 'bottom' ? 0 : width),
                    0, $window.width() - that.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);
            }
            if (!self.options.parent) {
                that.gainFocus();
            }
            that.$layer.show();
            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: [], // fixme: what's this?
                    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, {
                    foo: Ox.print(options, self.defaults.keyboard),
                    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?

        if (self.options.group && self.options.checked === null) {
            self.options.checked = false;
        }

        // 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') {
                that.$status.html(value ? oxui.symbols.check : '')
            } else if (key == 'disabled') {
                that.toggleClass('OxDisabled'); // fixme: this will only work if onChange is only invoked on actual change
            } else if (key == 'title') {
                self.options.title = Ox.makeArray(value);
                that.$title.html(self.options.title[0]);
            }
        }

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

        that.toggleChecked = function() {
            that.options({
                checked: !self.options.checked
            });
        };

        that.toggleDisabled = function() {

        };

        that.toggleTitle = function() {
            Ox.print('s.o.t', self.options.title)
            that.options({
                title: self.options.title.reverse()
            });
        };

        return that;

    };

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

    /**
    */
    Ox.CollapsePanel = function(options, self) {
        var self = self || {},
            that = new Ox.Panel({}, self)
                .defaults({
                    collapsed: false,
                    menu: [],
                    size: 16,
                    title: ''
                })
                .options(options)
                .addClass('OxCollapsePanel'),
            title = self.options.collapsed ?
                [{id: 'expand', title: 'expand'}, {id: 'collapse', title: 'collapse'}] :
                [{id: 'collapse', title: 'collapse'}, {id: 'expand', title: 'expand'}],
            $titlebar = new Ox.Bar({
                    orientation: 'horizontal',
                    size: self.options.size,
                })
                .dblclick(dblclickTitlebar)
                .appendTo(that),
            $switch = new Ox.Button({
                    id: self.options.id + 'Switch',
                    style: 'symbol',
                    title: title,
                    type: 'image',
                })
                .click(toggleCollapsed)
                .appendTo($titlebar),
            $title = new Ox.Element()
                .addClass('OxTitle')
                .html(self.options.title/*.toUpperCase()*/)
                .appendTo($titlebar);
        if (self.options.menu.length) {
            that.$menu = new Ox.Select({
                    id: self.options.id + 'Menu',
                    items: self.options.menu,
                    max: 0,
                    min: 0,
                    type: 'image'
                })
                .bindEvent('click', clickMenu)
                .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 clickMenu() {
            
        }
        function dblclickTitlebar(e) {
            if (!$(e.target).hasClass('OxButton')) {
                $switch.trigger('click');
            }
        }
        function toggleCollapsed() {
            var marginTop;
            self.options.collapsed = !self.options.collapsed;
            marginTop = self.options.collapsed ? -that.$content.height() : 0;
            that.$content.animate({
                marginTop: marginTop + 'px'
            }, 200);
        }
        self.onChange = function(key, value) {
            if (key == 'collapsed') {

            } else if (key == 'title') {
                $title.html(self.options.title);
            }
        };
        return that;
    };

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

    /**
        options:
            elements: [{            array of one, two or three elements
                collapsible: false, collapsible or not (only for outer elements)
                collapsed: false,   collapsed or not (only for collapsible elements)
                element: {},        OxElement (if any element is resizable or
                                    collapsible, all OxElements must have an id)
                resizable: false,   resizable or not (only for outer elements)
                resize: [],         array of sizes (only for resizable elements,
                                    first value is min, last value is max,
                                    other values are 'snappy' points in between)
                size: 0             size in px (one element must have no size)
            }],
            orientation: ''         'horizontal' or 'vertical'
        events:
            resize
            toggle
    */
    Ox.SplitPanel = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self) // fixme: Container
                .defaults({
                    elements: [],
                    orientation: 'horizontal'
                })
                .options(options || {})
                .addClass('OxSplitPanel');

        $.extend(self, {
            dimensions: oxui.getDimensions(self.options.orientation),
            edges: oxui.getEdges(self.options.orientation),
            length: self.options.elements.length,
            resizebarElements: [],
            $resizebars: []
        });

        // create elements
        that.$elements = [];
        self.options.elements.forEach(function(v, i) {
            self.options.elements[i] = $.extend({
                collapsible: false,
                collapsed: false,
                resizable: false,
                resize: [],
                size: 'auto'
            }, v);
            that.$elements[i] = v.element
                .css(self.edges[2], (parseInt(v.element.css(self.edges[2])) || 0) + 'px')
                .css(self.edges[3], (parseInt(v.element.css(self.edges[3])) || 0) + 'px');
            //alert(v.element.css(self.edges[3]))
        });

        // create resizebars
        self.options.elements.forEach(function(v, i) {
            //that.append(element)
            //Ox.print('V: ', v, that.$elements[i])
            var index = i == 0 ? 0 : 1;
            that.$elements[i].appendTo(that.$element); // fixme: that.$content
            if (v.collapsible || v.resizable) {
                Ox.print('v.size', v.size)
                self.resizebarElements[index] = i < 2 ? [0, 1] : [1, 2];
                self.$resizebars[index] = new Ox.Resizebar({
                    collapsible: v.collapsible,
                    edge: self.edges[index],
                    elements: [
                        that.$elements[self.resizebarElements[index][0]],
                        that.$elements[self.resizebarElements[index][1]]
                    ],
                    id: v.element.options('id'),
                    orientation: self.options.orientation == 'horizontal' ? 'vertical' : 'horizontal',
                    parent: that, // fixme: that.$content
                    resizable: v.resizable,
                    resize: v.resize,
                    size: v.size
                });
                self.$resizebars[index][i == 0 ? 'insertAfter' : 'insertBefore'](that.$elements[i]);
            }
        });

        self.options.elements.forEach(function(v, i) {
            v.collapsed && that.css(
                self.edges[i == 0 ? 0 : 1], -self.options.elements[i].size + 'px'
            );
        });

        setSizes(true);

        function getPositionById(id) {
            var position = -1;
            $.each(self.options.elements, function(i, element) {
                if (element.element.options('id') == id) {
                    position = i;
                    return false;
                }
            });
            Ox.print('getPositionById', id, position);
            return position;
        }

        function getSize(element) {
            return element.size + (element.collapsible || element.resizable);
            //return (element.size + (element.collapsible || element.resizable)) * !element.collapsed;
        }

        function getVisibleSize(element) {
            return getSize(element) * !element.collapsed;
        }

        function setSizes(init) {
            self.options.elements.forEach(function(v, i) {
                var edges = [
                    (init && parseInt(that.$elements[i].css(self.edges[0]))) || 0,
                    (init && parseInt(that.$elements[i].css(self.edges[1]))) || 0
                ];
                v.size != 'auto' && that.$elements[i].css(self.dimensions[0], v.size + 'px');
                if (i == 0) {
                    that.$elements[i].css(
                        self.edges[0], edges[0] + 'px'
                    );
                    that.$elements[i].css(
                        self.edges[1], (getSize(self.options.elements[1]) + (length == 3 ? getSize(self.options.elements[2]) : 0)) + 'px'
                    );
                } else if (i == 1) {
					that.$elements[i].css(
                        self.edges[0], self.options.elements[0].size == 'auto' ? 'auto' :
                        edges[0] + getSize(self.options.elements[0]) + 'px'
                    );
                    (self.options.elements[0].size != 'auto' || v.size != 'auto') && that.$elements[i].css(
                        self.edges[1], (self.length == 3 ? getSize(self.options.elements[2]) : 0) + 'px'
                    );
                } else {
                    that.$elements[i].css(
                        self.edges[0], (self.options.elements[1].size == 'auto' || v.size == 'auto') ? 'auto' :
                        (getVisibleSize(self.options.elements[0]) + getVisibleSize(self.options.elements[1])) + 'px'
                    );
                    that.$elements[i].css(
                        self.edges[1], edges[1] + 'px'
                    );
                }
                if (v.collapsible || v.resizable) {
                    self.$resizebars[i == 0 ? 0 : 1].css(self.edges[i == 0 ? 0 : 1], v.size);
                }
            });
        }

        that.isCollapsed = function(id) {
            var pos = Ox.isNumber(id) ? id : getPositionById(id);
            return self.options.elements[pos].collapsed;
        };

        that.replace = function(id, element) { // fixme: should be replaceElement
            // one can pass pos instead of id
            var pos = Ox.isNumber(id) ? id : getPositionById(id);
            Ox.print('replace', pos, element);
            Ox.print('element', self.options.elements[pos].element, element)
            that.$elements[pos] = element
                .css(self.edges[2], (parseInt(element.css(self.edges[2])) || 0) + 'px')
                .css(self.edges[3], (parseInt(element.css(self.edges[3])) || 0) + 'px');
            //alert(element.css(self.edges[3]))
            self.options.elements[pos].element.replaceWith(element.$element.$element || element.$element);
            self.options.elements[pos].element = element;
            setSizes();
            self.$resizebars.forEach(function($resizebar, i) {
                $resizebar.options({
                    elements: [
                        that.$elements[self.resizebarElements[i][0]],
                        that.$elements[self.resizebarElements[i][1]]
                    ]
                });
            });
            Ox.print(self.options.elements[pos])
            return that;
        };

        that.replaceElements = function(elements) {
            elements.forEach(function(element, i) {
                if (Ox.isNumber(element.size)) {
                    that.size(i, element.size);
                    if (element.collapsible || element.resizable) {
    				    self.$resizebars[i == 0 ? 0 : 1].options({
                            collapsible: element.collapsible,
                            resizable: element.resizable,
    				        size: element.size
    				    });
    				}
                }
                that.replace(i, element.element);
            });
            self.options.elements = elements;
            self.$resizebars.forEach(function($resizebar, i) {
                $resizebar.options({
                    elements: [
                        that.$elements[self.resizebarElements[i][0]],
                        that.$elements[self.resizebarElements[i][1]]
                    ]
                });
            });
            return that;
        }

        that.size = function(id, size) {
            // one can pass pos instead of id
            var pos = Ox.isNumber(id) ? id : getPositionById(id),
                element = self.options.elements[pos];
			if (arguments.length == 1) {
				return element.element[self.dimensions[0]]() * !that.isCollapsed(pos);
			} else {
				element.size = size;
	            setSizes();
	            return that;
			}
        };

        that.toggle = function(id) {
            // one can pass pos instead of id
            Ox.print('toggle', id);
            var pos = Ox.isNumber(id) ? id : getPositionById(id),
				element = self.options.elements[pos],
                value = parseInt(that.css(self.edges[pos == 0 ? 0 : 1])) +
					element.element[self.dimensions[0]]() *
					(element.collapsed ? 1 : -1),
                animate = {};
            animate[self.edges[pos == 0 ? 0 : 1]] = value;
            Ox.print('animate', animate, self.resizebarElements)
            that.animate(animate, 200, function() {
                element.collapsed = !element.collapsed;
                element.element.triggerEvent('toggle', {
                    'collapsed': element.collapsed
                });
				element = self.options.elements[pos == 0 ? 1 : pos - 1];
                element.element.triggerEvent(
					'resize',
					element.element[self.dimensions[0]]()
				);
            });
        };

        that.updateSize = function(pos, size) {
            // this is called from resizebar
            var pos = pos == 0 ? 0 : self.options.elements.length - 1; // fixme: silly that 0 or 1 is passed, and not pos
            self.options.elements[pos].size = size;
        }

        return that;

    };

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

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

    /**
    */
    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);
            return that;
        };
        that.stop = function() {
            that.animate({
                opacity: 0
            }, 250, function() {
                self.isRunning && clear();
                self.isRunning = false;
            });
            return that;
        }
        return that;
    }

    /**
    Ox.Progressbar
    */


    /*
    ============================================================================
    Video
    ============================================================================
    */

    /**
    */
    Ox.LargeTimeline = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    cuts: [],
                    duration: 0,
                    find: '',
                    matches: [],
                    points: [0, 0],
                    position: 0,
                    style: 'default',
                    subtitles: [],
                    videoId: '',
                    width: 0
                })
                .options(options || {})
                .addClass('OxTimelineLarge')
                .mousedown(mousedown)
                .mouseleave(mouseleave)
                .mousemove(mousemove);

        $.extend(self, {
            $cuts: [],
            $markerPoint: [],
            $subtitles: [],
            $tiles: {},
            $tooltip: new Ox.Tooltip(),
            center: parseInt(self.options.width / 2),
            element: that.$element[0],
            fps: 25,
            height: 64,
            tileWidth: 1500
        });
        self.tiles = self.options.duration * self.fps / self.tileWidth;

        self.$timeline = $('<div>')
            .css({
                left: self.center + 'px'
            })
            .appendTo(that.$element)

        $.each(self.options.subtitles, function(i, v) {
            self.$subtitles[i] = $('<div>')
                .addClass('OxSubtitle' + (self.options.matches.indexOf(i) > -1 ? ' OxHighlight' : ''))
                .css({
                    left: (v['in'] * self.fps) + 'px',
                    width: (((v['out'] - v['in']) * self.fps) - 4) + 'px'
                })
                .html(Ox.highlight(v.text, self.options.find))
                .appendTo(self.$timeline)
        });

        $.each(self.options.cuts, function(i, v) {
            self.$cuts[i] = $('<img>')
                .addClass('OxCut')
                .attr({
                    src: '/static/oxjs/build/png/ox.ui/videoMarkerCut.png'
                })
                .css({
                    left: (v * self.fps) + 'px'
                })
                .appendTo(self.$timeline)
        });

        self.$markerPosition = $('<img>')
            .addClass('OxMarkerPosition')
            .attr({
                src: '/static/oxjs/build/png/ox.ui/videoMarkerPlay.png'
            })
            .appendTo(that.$element);
        setMarker();

        $.each(['In', 'Out'], function(i, v) {
            self.$markerPoint[i] = $('<img>')
                .addClass('OxMarkerPoint' + v)
                .attr({
                    src: '/static/oxjs/build/png/ox.ui/videoMarker' + v + '.png'
                })
                .appendTo(self.$timeline);
            setMarkerPoint(i);
        });

        setWidth();
        setPosition();

        function mousedown(e) {
            var mousemove = false,
                x = e.clientX;
            $window.mousemove(function(e) {
                mousemove = true;
                self.options.position = Ox.limit(
                    self.options.position + (x - e.clientX) / self.fps,
                    0, self.options.duration
                );
                x = e.clientX;
                setPosition();
                that.triggerEvent('change', {
                    position: self.options.position
                });
            });
            $window.one('mouseup', function() {
                $window.unbind('mousemove');
                if (!mousemove) {
                    self.options.position = Ox.limit(
                        self.options.position + (e.clientX - that.$element.offset().left - self.center) / self.fps,
                        0, self.options.duration
                    );
                    setPosition();
                }
                that.triggerEvent('change', {
                    position: self.options.position
                });
            });
            e.preventDefault();
        }

        function mouseleave(e) {
            self.clientX = 0;
            self.clientY = 0;
            self.$tooltip.hide();
        }

        function mousemove(e) {
            self.clientX = e.clientX;
            self.clientY = e.clientY;
            updateTooltip();
        }

        function setMarkerPoint(i) {
            self.$markerPoint[i].css({
                left: (self.options.points[i] * self.fps) + 'px'
            });
        }

        function setMarker() {
            self.$markerPosition.css({
                left: (self.center - 4) + 'px',
            });
        }

        function setPosition() {
            self.tile = parseInt(self.options.position * self.fps / self.tileWidth);
            self.$timeline.css({
                marginLeft: (-self.options.position * self.fps) + 'px'
            });
            $.each(Ox.range(Math.max(self.tile - 1, 0), Math.min(self.tile + 2, self.tiles)), function(i, v) {
                if (!self.$tiles[v]) {
                    self.$tiles[v] = $('<img>')
                        .attr({
                            src: '/' + self.options.videoId + '/timelines/' +
                                (self.options.style == 'default' ? 'timeline' : self.options.style) + '.64.' + v + '.png'
                        })
                        .css({
                            left: (v * self.tileWidth) + 'px'
                        })
                        .appendTo(self.$timeline);
                }
            });
            if (self.clientX && self.clientY) {
                updateTooltip();
            }
        }

        function setWidth() {
            self.center = parseInt(self.options.width / 2);
            that.css({
                width: self.options.width + 'px'
            });
            self.$timeline.css({
                left: self.center + 'px'
            });
            setMarker();
        }

        function updateTooltip() {
            var position = self.options.position + (self.clientX - that.offset().left - self.center) / self.fps;
            if (position >= 0 && position <= self.options.duration) {
                self.$tooltip
                    .options({
                        title: Ox.formatDuration(position, 3)
                    })
                    .show(self.clientX, self.clientY);
            } else {
                self.$tooltip.hide();
            }
        }

        self.onChange = function(key, value) {
            if (key == 'points') {
                setMarkerPoint(0);
                setMarkerPoint(1);
            } else if (key == 'position') {
                setPosition();
            } else if (key == 'width') {
                setWidth();
            }
        };

        return that;

    };

    /**
    */
    Ox.SmallTimeline = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    cuts: [],
                    duration: 0,
                    find: '',
                    matches: [],
                    points: [0, 0],
                    position: 0,
                    subtitles: [],
                    videoId: '',
                    width: 0
                })
                .options(options || {})
                .addClass('OxTimelineSmall')
                .mousedown(mousedown)
                .mouseleave(mouseleave)
                .mousemove(mousemove);

        $.extend(self, {
            $images: [],
            $lines: [],
            $markerPoint: [],
            $subtitles: [],
            hasSubtitles: self.options.subtitles.length,
            height: 16,
            lines: Math.ceil(self.options.duration / self.options.width),
            margin: 8
        });

		that.css({
			width: (self.options.width + self.margin) + 'px',
			height: ((self.height + self.margin) * self.lines + 4) + 'px'
		});

        getTimelineImageURL(function(url) {

            self.timelineImageURL = url;

            $.each(Ox.range(0, self.lines), function(i) {
                addLine(i);
            });

            self.$markerPosition = $('<img>')
                .addClass('OxMarkerPosition')
                .attr({
                    src: '/static/oxjs/build/png/ox.ui/videoMarkerPlay.png'
                })
                .css({
                    position: 'absolute',
                    width: '9px',
                    height: '5px',
                    zIndex: 10
                })
                .appendTo(that.$element);

            setPosition();

            $.each(['in', 'out'], function(i, v) {
                var titleCase = Ox.toTitleCase(v);
                self.$markerPoint[i] = $('<img>')
                    .addClass('OxMarkerPoint' + titleCase)
                    .attr({
                        src: '/static/oxjs/build/png/ox.ui/videoMarker' + titleCase + '.png'
                    })
                    .appendTo(that.$element);
                setMarkerPoint(i);
            });

        });

        function addLine(i) {
            self.$lines[i] = new Ox.Element('div')
                .css({
                    top: i * (self.height + self.margin) + 'px',
                    width: self.options.width + 'px'
                })
                .appendTo(that);
            self.$images[i] = $('<img>')
                .addClass('OxTimelineSmallImage')
                .attr({
                    src: self.timelineImageURL
                })
                .css({
                    marginLeft: (-i * self.options.width) + 'px'
                })
                .appendTo(self.$lines[i].$element)
            if (self.hasSubtitles) {
                self.subtitlesImageURL = getSubtitlesImageURL();
                self.$subtitles[i] = $('<img>')
                    .attr({
                        src: self.subtitlesImageURL
                    })
                    .css({
                        marginLeft: (-i * self.options.width) + 'px'
                    })
                    .appendTo(self.$lines[i].$element);
            }
        }

        function getPosition(e) {
            //FIXME: this might still be broken in opera according to http://acko.net/blog/mouse-handling-and-absolute-positions-in-javascript
            return (e.offsetX ? e.offsetX : e.clientX - $(e.target).offset().left);
        }

        function getSubtitle(position) {
            var subtitle = null;
            $.each(self.options.subtitles, function(i, v) {
                if (v['in'] <= position && v['out'] >= position) {
                    subtitle = v;
                    return false;
                }
            });
            return subtitle;
        }

        function getSubtitlesImageURL() {
            var height = 18,
                width = Math.ceil(self.options.duration),
                $canvas = $('<canvas>')
                    .attr({
                        height: height,
                        width: width
                    }),
                canvas = $canvas[0],
                context = canvas.getContext('2d'),
                imageData = context.createImageData(width, height),
                data = imageData.data;
            $.each(self.options.subtitles, function(i, v) {
                var color = self.options.matches.indexOf(i) > -1 ? [255, 255, 0] : [255, 255, 255]
                $.each(Ox.range(
                    Math.round(v['in']),
                    Math.round(v['out']) + 1
                ), function(i, x) {
                    $.each(Ox.range(0, 18), function(i, y) {
                        var index = x * 4 + y * 4 * width;
                        data[index] = color[0];
                        data[index + 1] = color[1];
                        data[index + 2] = color[2];
                        data[index + 3] = (y == 0 || y == 17) ? 255 : 128
                    });
                });
            });
            context.putImageData(imageData, 0, 0);
            return canvas.toDataURL();
        }

        function getTimelineImageURL(callback) {
            var height = 16,
                images = Math.ceil(self.options.duration / 3600),
                loaded = 0,
                width = Math.ceil(self.options.duration),
                $canvas = $('<canvas>')
                    .attr({
                        height: height,
                        width: width
                    }),
                canvas = $canvas[0],
                context = canvas.getContext('2d');
            Ox.range(images).forEach(function(i) {
                var $img = $('<img>')
                    .attr({
                        src: '/' + self.options.videoId + '/timelines/timeline.16.' + i + '.png'
                    })
                    .load(function() {
                        context.drawImage($img[0], i * 3600, 0);
                        Ox.print('loaded, images', loaded, images, $img[0])
                        if (++loaded == images) {
                            Ox.print('callback', canvas.toDataURL().length)
                            callback(canvas.toDataURL());
                        }
                    });
            });
        }

        function mousedown(e) {
            if ($(e.target).is('img')) {
                self.options.position = getPosition(e);
                setPosition();
                that.triggerEvent('change', {
                    position: self.options.position
                });
            }
            $window.mousemove(function(e) {
                if ($(e.target).is('img')) {
                    self.options.position = getPosition(e);
                    setPosition();
                    that.triggerEvent('change', {
                        position: self.options.position
                    });
                }
            });
            $window.one('mouseup', function() {
                $window.unbind('mousemove');
            })
            e.preventDefault();
        }

        function mouseleave(e) {
            self.$tooltip.hide();
        }

        function mousemove(e) {
            var $target = $(e.target),
                position,
                subtitle;
            if ($target.is('img')) {
                //FIXME: this might still be broken in opera according to http://acko.net/blog/mouse-handling-and-absolute-positions-in-javascript
                position = getPosition(e),
                subtitle = getSubtitle(position);
                Ox.print('position', position, e)
                self.$tooltip = new Ox.Tooltip({
                        title: subtitle ?
                            '<span class=\'OxBright\'>' +
                            Ox.highlight(subtitle.text, self.options.find).replace(/\n/g, '<br/>') + '</span><br/>' +
                            Ox.formatDuration(subtitle['in'], 3) + ' - ' + Ox.formatDuration(subtitle['out'], 3) :
                            Ox.formatDuration(position, 3)
                    })
                    .css({
                        textAlign: 'center'
                    })
                    .show(e.clientX, e.clientY);
            }

        }

        function setMarker() {
            self.$markerPosition
                .css({
                    left: (self.options.position % self.options.width) + 'px',
                    top: (parseInt(self.options.position / self.options.width) * (self.height + self.margin) + 2) + 'px',
                });
        }

        function setMarkerPoint(i) {
            var position = self.options.points[i];
            self.$markerPoint[i]
                .css({
                    left: (position % self.options.width) + 'px',
                    top: (parseInt(position / self.options.width) * (self.height + self.margin) + 16) + 'px',
                });
        }

        function setPosition() {
            self.options.position = Ox.limit(self.options.position, 0, self.options.duration);
            setMarker();
        }

        function setWidth() {
            self.lines = Math.ceil(self.options.duration / self.options.width);
			that.css({
				width: (self.options.width + self.margin) + 'px',
				height: ((self.height + self.margin) * self.lines + 4) + 'px'
			});
            $.each(Ox.range(self.lines), function(i) {
                if (self.$lines[i]) {
                    self.$lines[i].css({
                        width: self.options.width + 'px'
                    });
                    self.$images[i].css({
                        marginLeft: (-i * self.options.width) + 'px'
                    });
                    if (self.hasSubtitles) {
                        self.$subtitles[i].css({
                            marginLeft: (-i * self.options.width) + 'px'
                        });
                    }
                } else {
                    addLine(i);
                }
            });
            while (self.$lines.length > self.lines) {
                self.$lines[self.$lines.length - 1].remove();
                self.$lines.pop();
            }
            setMarker();
            setMarkerPoint(0);
            setMarkerPoint(1);
        }

        self.onChange = function(key, value) {
            Ox.print('onChange:', key, value)
            if (key == 'points') {
                Ox.print('key', key, 'value', value)
                setMarkerPoint(0);
                setMarkerPoint(1);
            } else if (key == 'position') {
                setPosition();
            } else if (key == 'width') {
                setWidth();
            }
        };

        return that;

    };

    /**
    */
    Ox.VideoEditor = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    cuts: [],
                    duration: 0,
                    find: '',
                    frameURL: function() {},
					height: 0,
                    largeTimeline: true,
                    matches: [],
                    points: [0, 0],
                    position: 0,
                    posterFrame: 0,
                    subtitles: [],
                    videoHeight: 0,
                    videoId: '',
                    videoWidth: 0,
                    videoSize: 'small',
                    videoURL: '',
                    width: 0
                })
                .options(options || {})
                .addClass('OxEditor')
                /*.css({
					//height: self.options.height + 'px',
					overflowY: 'scroll',
					//width: self.options.width + 'px'
				});*/

        that.click(that.gainFocus)

        $.extend(self, {
            $player: [],
            $timeline: [],
            controlsHeight: 16,
            margin: 8,
            videoRatio: self.options.videoWidth / self.options.videoHeight
        });
        self.sizes = getSizes();

        $.each(['play', 'in', 'out'], function(i, type) {
            self.$player[i] = new Ox.VideoPlayer({
                    duration: self.options.duration,
                    find: self.options.find,
                    height: self.sizes.player[i].height,
                    id: 'player' + Ox.toTitleCase(type),
                    points: self.options.points,
                    position: type == 'play' ? self.options.position : self.options.points[type == 'in' ? 0 : 1],
                    posterFrame: self.options.posterFrame,
                    subtitles: self.options.subtitles,
                    type: type,
                    url: type == 'play' ? self.options.videoURL : self.options.frameURL,
                    width: self.sizes.player[i].width
                })
                .css({
                    left: self.sizes.player[i].left + 'px',
                    top: self.sizes.player[i].top + 'px'
                })
                .appendTo(that);
            if (type == 'in' || type == 'out') {
                self.$player[i].bindEvent({
                    change: function() {
                        goToPoint(type);
                    },
                    set: function() {
                        setPoint(type);
                    }
                })
            }
        });
        self.$player[0].bindEvent({
            change: changePlayer,
            togglesize: togglePlayerSize
        });

        self.$timeline[0] = new Ox.LargeTimeline({
                cuts: self.options.cuts,
                duration: self.options.duration,
                find: self.options.find,
                id: 'timelineLarge',
                matches: self.options.matches,
                points: self.options.points,
                position: self.options.position,
                subtitles: self.options.subtitles,
                videoId: self.options.videoId,
                width: self.sizes.timeline[0].width
            })
            .css({
                left: self.sizes.timeline[0].left + 'px',
                top: self.sizes.timeline[0].top + 'px'
            })
            .bindEvent('change', changeTimelineLarge)
            .bindEvent('changeEnd', changeTimelineLarge)
            .appendTo(that);

        self.$timeline[1] = new Ox.SmallTimeline({
                cuts: self.options.cuts,
                duration: self.options.duration,
                find: self.options.find,
                id: 'timelineSmall',
                matches: self.options.matches,
                points: self.options.points,
                position: self.options.position,
                subtitles: self.options.subtitles,
                videoId: self.options.videoId,
                width: self.sizes.timeline[1].width
            })
            .css({
                left: self.sizes.timeline[1].left + 'px',
                top: self.sizes.timeline[1].top + 'px'
            })
            .bindEvent('change', changeTimelineSmall)
            .appendTo(that);

        that.bindEvent({
            key_alt_left: function() {
                movePositionTo('cut', -1);
            },
            key_alt_right: function() {
                movePositionTo('cut', 1);
            },
            key_alt_shift_left: function() {
            },
            key_alt_shift_right: function() {
            },
            key_closebracket: function() {
                goToPoint('out');
            },
            key_comma: function() {
                movePositionTo('subtitle', -1);
            },
            key_dot: function() {
                movePositionTo('subtitle', 1);
            },
            key_down: function() {
                movePositionBy(self.options.width - 2 * self.margin);
            },
            key_i: function() {
                setPoint('in');
            },
            key_left: function() {
                movePositionBy(-1);
            },
            key_m: toggleMute,
            key_o: function() {
                setPoint('out');
            },
            key_openbracket: function() {
                goToPoint('in');
            },
            key_p: playInToOut,
            key_right: function() {
                movePositionBy(1);
            },
            key_shift_comma: function() {
                movePositionTo('match', -1)
            },
            key_shift_dot: function() {
                movePositionTo('match', 1)
            },
            key_shift_down: function() {
                movePositionBy(self.options.duration);
            },
            key_shift_left: function() {
                movePositionBy(-0.04);
                //movePositionBy(-60);
            },
            key_shift_right: function() {
                movePositionBy(0.04);
                //movePositionBy(60);
            },
            key_shift_up: function() {
                movePositionBy(-self.options.duration);
            },
            key_space: togglePlay,
            key_up: function() {
                movePositionBy(-(self.options.width - 2 * self.margin));
            }
        });

        // that.gainFocus();

        function changePlayer(event, data) {
            self.options.position = data.position;
            self.$timeline[0].options({
                position: data.position
            });
            self.$timeline[1].options({
                position: data.position
            });
        }

        function changeTimelineLarge(event, data) {
            self.options.position = data.position;
            self.$player[0].options({
                position: data.position
            });
            self.$timeline[1].options({
                position: data.position
            });
        }

        function changeTimelineSmall(event, data) {
            self.options.position = data.position;
            self.$player[0].options({
                position: data.position
            });
            self.$timeline[0].options({
                position: data.position
            });
        }

        function getNextPosition(type, direction) {
            var found = false,
                position = 0,
                positions;
            if (type == 'cut') {
                positions = self.options.cuts;
            } else if (type == 'match') {
                positions = $.map(self.options.matches, function(v, i) {
                    return self.options.subtitles[v]['in'];
                });
            } else if (type == 'subtitle') {
                positions = $.map(self.options.subtitles, function(v, i) {
                    return v['in'];
                });
            }
            direction == -1 && positions.reverse();
            $.each(positions, function(i, v) {
                if (direction == 1 ? v > self.options.position : v < self.options.position) {
                    position = v;
                    found = true;
                    return false;
                }
            });
            direction == -1 && positions.reverse();
            if (!found) {
                position = positions[direction == 1 ? 0 : positions.length - 1];
            }
            return position;
        }

        function getSizes(scrollbarIsVisible) {
            Ox.print('getSizes', scrollbarIsVisible)
            var scrollbarWidth = oxui.scrollbarSize,
				contentWidth = self.options.width - (scrollbarIsVisible ? scrollbarWidth : 0),
                lines,
				size = {
                    player: [],
                    timeline: []
                },
                width, widths;
            if (self.options.videoSize == 'small') {
                width = 0;
                widths = Ox.divideInt(contentWidth - 4 * self.margin, 3);
                [1, 0, 2].forEach(function(v, i) {
                    size.player[v] = {
                        left: (i + 0.5) * self.margin + width,
                        top: self.margin / 2,
                        width: widths[i],
                        height: Math.round(widths[1] / self.videoRatio)
                    }
                    width += widths[i];
                });
            } else {
                size.player[0] = {
                    left: self.margin / 2,
                    top: self.margin / 2,
                    width: Math.round((contentWidth - 3 * self.margin + (self.controlsHeight + self.margin) / 2 * self.videoRatio) * 2/3),
                }
                size.player[0].height = Math.round(size.player[0].width / self.videoRatio);
                size.player[1] = {
                    left: size.player[0].left + size.player[0].width + self.margin,
                    top: size.player[0].top,
                    width: contentWidth - 3 * self.margin - size.player[0].width
                }
                size.player[1].height = Math.ceil(size.player[1].width / self.videoRatio)
                size.player[2] = {
                    left: size.player[1].left,
                    top: size.player[0].top + size.player[1].height + self.controlsHeight + self.margin,
                    width: size.player[1].width,
                    height: size.player[0].height - size.player[1].height - self.controlsHeight - self.margin
                }
            }
            size.timeline[0] = {
                left: self.margin / 2,
                top: size.player[0].height + self.controlsHeight + 1.5 * self.margin,
                width: contentWidth - 2 * self.margin,
                height: 64
            }
            size.timeline[1] = {
                left: size.timeline[0].left,
                top: size.timeline[0].top + size.timeline[0].height + self.margin,
                width: size.timeline[0].width
            }
            lines = Math.ceil(self.options.duration / size.timeline[1].width);
            Ox.print('lines', lines, getHeight(), self.options.height, (scrollbarIsVisible && getHeight() <= self.options.height) ? 'scroll' : 'auto')
            that.css({
                overflowY: (scrollbarIsVisible && getHeight() <= self.options.height) ? 'scroll' : 'auto'
            });
            return (!scrollbarIsVisible && getHeight() > self.options.height) ? getSizes(true) : size;
            function getHeight() {
                return size.player[0].height + self.controlsHeight +
                    size.timeline[0].height + lines * 16 +
                    (lines + 3) * self.margin;
            }
        }

        function goToPoint(point) {
            self.options.position = self.options.points[point == 'in' ? 0 : 1];
            setPosition();
            that.triggerEvent('change', {
                position: self.options.position
            });
        }

        function movePositionBy(sec) {
            self.options.position = Ox.limit(self.options.position + sec, 0, self.options.duration);
            setPosition();
            that.triggerEvent('change', {
                position: self.options.position
            });
        }

        function movePositionTo(type, direction) {
            self.options.position = getNextPosition(type, direction);
            setPosition();
            that.triggerEvent('change', {
                position: self.options.position
            });
        }

        function playInToOut() {
            self.$player[0].playInToOut();
        }

        function resizeEditor(event, data) {
            var width = data - 2 * margin + 100;
            resizeVideoPlayers(width);
            $timelineLarge.options({
                width: width
            });
            $timelineSmall.options({
                width: width
            });
        }

        function resizePlayers() {
            $.each(self.$player, function(i, v) {
                v.options({
                    width: size[i].width,
                    height: size[i].height
                })
                .css({
                    left: size[i].left + 'px',
                    top: size[i].top + 'px',
                });
            });
        }

        function setPoint(point) {
            self.options.points[point == 'in' ? 0 : 1] = self.options.position;
            self.$player[point == 'in' ? 1 : 2].options({
                position: self.options.position
            });
            if (self.options.points[1] < self.options.points[0]) {
                self.options.points[point == 'in' ? 1 : 0] = self.options.position;
                self.$player[point == 'in' ? 2 : 1].options({
                    position: self.options.position
                });
            }
            $.each(self.$player, function(i, v) {
                v.options({
                    points: self.options.points
                });
            });
            $.each(self.$timeline, function(i, v) {
                Ox.print('points:', self.options.points)
                v.options({
                    points: self.options.points
                });
            });
        }

        function setPosition() {
            self.$player[0].options({
                position: self.options.position
            });
            $.each(self.$timeline, function(i, v) {
                v.options({
                    position: self.options.position
                });
            });
        }

        function setSizes() {
            self.sizes = getSizes();
            $.each(self.$player, function(i, v) {
                v.options({
                    height: self.sizes.player[i].height,
                    width: self.sizes.player[i].width
                })
                .css({
                    left: self.sizes.player[i].left + 'px',
                    top: self.sizes.player[i].top + 'px'
                });
            });
            $.each(self.$timeline, function(i, v) {
                v.options({
                    width: self.sizes.timeline[i].width
                })
                .css({
                    left: self.sizes.timeline[i].left + 'px',
                    top: self.sizes.timeline[i].top + 'px'
                });
            });
        }

        function toggleMute() {
            self.$player[0].toggleMute();
        }

        function togglePlay() {
            self.$player[0].togglePlay();
        }

        function togglePlayerSize() {
            self.options.videoSize = self.options.videoSize == 'large' ? 'small' : 'large';
            setSizes();
        }

        self.onChange = function(key, value) {
            if (key == 'width' || key == 'height') {
                Ox.print('XXXX setSizes', key, value, self.options.width, self.options.height)
                setSizes();
            }
        };

        return that;

    };

    /**
    */
    Ox.VideoPlayer = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    find: '',
                    height: 0,
                    points: [0, 0],
                    position: 0,
                    posterFrame: 0,
                    subtitles: [],
                    type: 'play',
                    url: '',
                    width: 0
                })
                .options(options || {})
                .addClass('OxVideoPlayer')
                .css({
                    height: (self.options.height + 16) + 'px',
                    width: self.options.width + 'px'
                });

        self.controlsHeight = 16;

        if (self.options.type == 'play') {
            self.$video = $('<video>')
                .attr({
                    preload: 'auto',
                    src: self.options.url
                })
                .css({
                    height: self.options.height + 'px',
                    width: self.options.width + 'px'
                })
                .appendTo(that.$element);
            self.video = self.$video[0];
        } else {
            self.$video = $('<img>')
                .css({
                    height: self.options.height + 'px',
                    width: self.options.width + 'px'
                })
                .appendTo(that.$element)
        }

        self.$subtitle = $('<div>')
            .addClass('OxSubtitle')
            .appendTo(that.$element);

        setSubtitleSize();

        self.$markerFrame = $('<div>')
            .addClass('OxMarkerFrame')
            .append(
                $('<div>')
                    .addClass('OxFrame')
                    .css({
                        width: Math.floor((self.options.width - self.options.height) / 2) + 'px',
                        height: self.options.height + 'px'
                    })
            )
            .append(
                $('<div>')
                    .addClass('OxPoster')
                    .css({
                        width: (self.options.height - 2) + 'px',
                        height: (self.options.height - 2) + 'px'
                    })
            )
            .append(
                $('<div>')
                    .addClass('OxFrame')
                    .css({
                        width: Math.ceil((self.options.width - self.options.height) / 2) + 'px',
                        height: self.options.height + 'px'
                    })
            )
            .hide()
            .appendTo(that.$element);

        self.$markerPoint = {}
        $.each(['in', 'out'], function(i, point) {
            self.$markerPoint[point] = {};
            $.each(['top', 'bottom'], function(i, edge) {
                var titleCase = Ox.toTitleCase(point) + Ox.toTitleCase(edge);
                self.$markerPoint[point][edge] = $('<img>')
                    .addClass('OxMarkerPoint OxMarker' + titleCase)
                    .attr({
                        src: '/static/oxjs/build/png/ox.ui/videoMarker' + titleCase + '.png' // fixme: remove static path
                    })
                    .hide()
                    .appendTo(that.$element);
                if (self.options.points[point == 'in' ? 0 : 1] == self.options.position) {
                    self.$markerPoint[point][edge].show();
                }
            });
        });

        self.$controls = new Ox.Bar({
                size: self.controlsHeight
            })
            .css({
                marginTop: '-2px'
            })
            .appendTo(that);

        if (self.options.type == 'play') {
            // fixme: $buttonPlay etc.
            self.$playButton = new Ox.Button({
                    id: self.options.id + 'Play',
                    title: [
                        {id: 'play', title: 'play'},
                        {id: 'pause', title: 'pause'}
                    ],
                    tooltip: ['Play', 'Pause'],
                    type: 'image'
                })
                .bindEvent('click', togglePlay)
                .appendTo(self.$controls);
            self.$playInToOutButton = new Ox.Button({
                    id: self.options.id + 'PlayInToOut',
                    title: 'PlayInToOut',
                    tooltip: 'Play In to Out',
                    type: 'image'
                })
                .bindEvent('click', function() {
                    that.playInToOut();
                })
                .appendTo(self.$controls);
            self.$muteButton = new Ox.Button({
                    id: self.options.id + 'Mute',
                    title: [
                        {id: 'mute', title: 'mute'},
                        {id: 'unmute', title: 'unmute'}
                    ],
                    tooltip: ['Mute', 'Unmute'],
                    type: 'image'
                })
                .bindEvent('click', toggleMute)
                .appendTo(self.$controls);
            self.$sizeButton = new Ox.Button({
                    id: self.options.id + 'Size',
                    title: [
                        {id: 'large', title: 'add'},
                        {id: 'small', title: 'remove'}
                    ],
                    tooltip: ['Larger', 'Smaller'],
                    type: 'image'
                })
                .bindEvent('click', toggleSize)
                .appendTo(self.$controls);
        } else {
            self.$goToPointButton = new Ox.Button({
                    id: self.options.id + 'GoTo' + Ox.toTitleCase(self.options.type),
                    title: 'GoTo' + Ox.toTitleCase(self.options.type),
                    tooltip: 'Go to ' + Ox.toTitleCase(self.options.type) + ' Point',
                    type: 'image'
                })
                .bindEvent('click', goToPoint)
                .appendTo(self.$controls);
            self.$setPointButton = new Ox.Button({
                    id: self.options.id + 'Set' + Ox.toTitleCase(self.options.type),
                    title: 'Set' + Ox.toTitleCase(self.options.type),
                    tooltip: 'Set ' + Ox.toTitleCase(self.options.type) + ' Point',
                    type: 'image'
                })
                .bindEvent('click', setPoint)
                .appendTo(self.$controls);
        }

        self.$positionInput = new Ox.TimeInput({
                milliseconds: true,
                seconds: true,
                value: Ox.formatDuration(self.options.position, 3)
            })
            .css({
                float: 'right',
            })
            .appendTo(self.$controls)

        self.$positionInput.css({
            width: '98px'
        });
        // fixme: children doesnt work w/o $element
        self.$positionInput.$element.children('.OxLabel').each(function(i, element) {
            $(this).css({
                width: '22px',
                marginLeft: (i == 0 ? 8 : 0) + 'px',
                background: 'rgb(32, 32, 32)'
            });
        });
        self.$positionInput.$element.children('div.OxInput').each(function(i) {
            var marginLeft = [-82, -58, -34, -10];
            $(this).css({
                marginLeft: marginLeft[i] + 'px'
            }).addClass('foo');
        });

        if (self.options.type == 'play') {
            self.$loadingIcon = new Ox.LoadingIcon()
                .appendTo(that)
                .start();
            self.loadingInterval = setInterval(function() {
                if (self.video.readyState) {
                    clearInterval(self.loadingInterval);
                    self.$loadingIcon.stop();
                    setPosition();
                }
            }, 50);
        } else {
            setPosition();
        }

        function getSubtitle() {
            var subtitle = '';
            $.each(self.options.subtitles, function(i, v) {
                if (v['in'] <= self.options.position && v['out'] > self.options.position) {
                    subtitle = v.text;
                    return false;
                }
            });
            return subtitle;
        }

        function goToPoint() {
            that.triggerEvent('change', {
                position: self.options.points[self.options.type == 'in' ? 0 : 1]
            });
        }

        function playing() {
            self.options.position = Math.round(self.video.currentTime * 25) / 25;
            if (self.video.ended) {
                self.$playButton.trigger('click');
            }
            if (self.playingInToOut && self.options.position >= self.options.points[1])  {
                self.$playButton.trigger('click');
                self.options.position = self.options.points[1];
            }
            setMarkers();
            setSubtitle();
            self.$positionInput.options({
                value: Ox.formatDuration(self.options.position, 3)
            });
            that.triggerEvent('change', {
                position: self.options.position
            });
        }

        function setHeight() {
            that.css({
                height: (self.options.height + 16) + 'px'
            });
            self.$video.css({
                height: self.options.height + 'px'
            });
            setSubtitleSize();
        }

        function setMarkers() {
            self.options.position == self.options.posterFrame ? self.$markerFrame.show() : self.$markerFrame.hide();
            $.each(self.$markerPoint, function(point, markers) {
                $.each(markers, function(edge, marker) {
                    self.options.position == self.options.points[point == 'in' ? 0 : 1] ?
                            marker.show() : marker.hide();
                });
            })
        }

        function setPoint() {
            var data = {};
            self.options.points[self.options.type == 'in' ? 0 : 1] = self.options.position;
            setMarkers();
            data[self.options.type] = self.options.position;
            that.triggerEvent('set', data);
        }

        function setPosition() {
            if (self.options.type == 'play') {
                self.video.currentTime = self.options.position;
            } else {
				self.$loadingIcon && self.$loadingIcon.stop();
				if (self.$video.attr('src') != self.options.url(self.options.position)) {
					self.$loadingIcon = new Ox.LoadingIcon()
	                    .appendTo(that)
	                    .start();
	                self.$video.attr({
	                        src: self.options.url(self.options.position)
	                    })
	                    .load(self.$loadingIcon.stop);
				}
            }
            setMarkers();
            setSubtitle();
            self.$positionInput.options({
                value: Ox.formatDuration(self.options.position, 3)
            });
        }

        function setSubtitle() {
            var subtitle = getSubtitle();
            if (subtitle != self.subtitle) {
                self.subtitle = subtitle;
                self.$subtitle.html(Ox.highlight(self.subtitle, self.options.find).replace(/\n/g, '<br/>'));
            }
        }

        function setSubtitleSize() {
            self.$subtitle.css({
                bottom: parseInt(self.controlsHeight + self.options.height / 16) + 'px',
                width: self.options.width + 'px',
                fontSize: parseInt(self.options.height / 20) + 'px',
                WebkitTextStroke: (self.options.height / 1000) + 'px rgb(0, 0, 0)'
            });
        }

        function setWidth() {
            that.css({
                width: self.options.width + 'px'
            });
            self.$video.css({
                width: self.options.width + 'px'
            });
            setSubtitleSize();
        }

        function toggleMute() {
            self.video.muted = !self.video.muted;
        }

        function togglePlay() {
            self.video.paused ? that.play() : that.pause();
        }

        function toggleSize(event, data) {
            that.triggerEvent('togglesize', {
                size: data.id
            });
        }

        self.onChange = function(key, value) {
            if (key == 'height') {
                setHeight();
            } else if (key == 'points') {
                setMarkers();
            } else if (key == 'position') {
                setPosition();
            } else if (key == 'posterFrame') {
                setMarkers();
            } else if (key == 'width') {
                setWidth();
            }
        }

        that.mute = function() {
            self.video.muted = true;
            return that;
        };

        that.pause = function() {
            self.video.pause();
            clearInterval(self.playInterval);
            self.playingInToOut = false;
            return that;
        };

        that.play = function() {
            self.video.play();
            self.playInterval = setInterval(playing, 40);
            return that;
        };

        that.playInToOut = function() {
            self.options.position = self.options.points[0];
            setPosition();
            Ox.print('sop', self.options.position, self.options.points);
            self.playingInToOut = true;
            self.video.paused && self.$playButton.trigger('click');
            return that;
        };

        that.toggleMute = function() {
            self.$muteButton.trigger('click');
            return that;
        }

        that.togglePlay = function() {
            self.$playButton.trigger('click');
            return that;
        }

        that.unmute = function() {
            self.video.muted = false;
            return that;
        };

        return that;

    };


    /*
    ============================================================================
    Miscellaneous
    ============================================================================
    */

    /**
    */
    Ox.Tooltip = function(options, self) {

        var self = self || {},
            that = new Ox.Element('div', self)
                .defaults({
                    title: ''
                })
                .options(options || {})
                .addClass('OxTooltip')
                .html(self.options.title);

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

        that.hide = function() {
            that.animate({
                opacity: 0
            }, 0, function() {
                that.remove();
            });
            return that;
        };

        that.show = function(x, y) {
            var left, top, width, height;
            $('.OxTooltip').remove(); // fixme: don't use dom
            that.appendTo($body);
            width = that.width();
            height = that.height();
            left = Ox.limit(x - width / 2, 0, $document.width() - width);
            top = y > $document.height() - height - 16 ? y - 32 : y + 16;
            that.css({
                    left: left + 'px',
                    top: top + 'px'
                })
                .animate({
                    opacity: 1
                }, 0);
            return that;
        };

        return that;

    };

})();
