    /**
     * @extends Ext.ux.event.Driver
     * Event recorder.
     */
    Ext.define('Ext.ux.event.Recorder', function () {
        function samePt (pt1, pt2) {
            return pt1.x == pt2.x && pt1.y == pt2.y;
        }

        return {
            extend: 'Ext.ux.event.Driver',

            eventsToRecord: function () {
                var key = { kind: 'keyboard', modKeys: true, key: true, bubbles: true },
                    mouse = { kind: 'mouse', button: true, modKeys: true, xy: true, bubbles: true };

                return {
                    keydown: key,
                    keypress: key,
                    keyup: key,

                    mousemove: mouse,
                    mouseover: mouse,
                    mouseout: mouse,
                    click: mouse,
                    //mousewheel: Ext.apply({ wheel: true }, mouse),
                    mousedown: mouse,
                    mouseup: mouse,

                    scroll: { kind: 'misc', bubbles: false }
                };
            }(),

            ignoreIdRegEx: /ext-gen(?:\d+)/,

            constructor: function (config) {
                var me = this,
                    events = config && config.eventsToRecord;

                if (events) {
                    me.eventsToRecord = Ext.apply(Ext.apply({}, me.eventsToRecord), // duplicate
                                            events); // and merge
                    delete config.eventsToRecord; // don't smash
                }

                me.callParent(arguments);

                me.addEvents(
                    /**
                     * @event add
                     * Fires when an event is added to the recording.
                     * @param {Ext.ux.event.Recorder} this
                     * @param {Object} eventDescriptor The event descriptor.
                     */
                    'add',

                    /**
                     * @event coalesce
                     * Fires when an event is coalesced. This edits the tail of the recorded
                     * event list.
                     * @param {Ext.ux.event.Recorder} this
                     * @param {Object} eventDescriptor The event descriptor that was coalesced.
                     */
                    'coalesce'
                );

                me.clear();
                me.modKeys = [];
                me.attachTo = me.attachTo || window;
            },

            clear: function () {
                this.eventsRecorded = [];
            },

            coalesce: function (rec) {
                var me = this,
                    events = me.eventsRecorded,
                    length = events.length,
                    tail = length && events[length-1],
                    tailPrev;

                if (!tail) {
                    return false;
                }

                if (rec.type == 'mousemove') {
                    if (tail.type == 'mousemove' && rec.ts - tail.ts < 200) {
                        rec.ts = tail.ts;
                        events[length-1] = rec;
                        return true;
                    }
                } else if (rec.type == 'click') {
                    if (length >= 2 && tail.type == 'mouseup' &&
                            (tailPrev = events[length-2]).type == 'mousedown') {
                        if (rec.button == tail.button && rec.button == tailPrev.button &&
                                rec.target == tail.target && rec.target == tailPrev.target &&
                                samePt(rec, tail) && samePt(rec, tailPrev) ) {
                            events.pop(); // remove mouseup
                            tailPrev.type = 'mduclick';
                            return true;
                        }
                    }
                }

                return false;
            },

            getElementXPath: function (el) {
                var me = this,
                    good = false,
                    xpath = [],
                    count,
                    sibling,
                    t,
                    tag;

                for (t = el; t; t = t.parentNode) {
                    if (t == me.attachTo.document.body) {
                        xpath.unshift('~');
                        good = true;
                        break;
                    }
                    if (t.id && !me.ignoreIdRegEx.test(t.id)) {
                        xpath.unshift('#' + t.id);
                        good = true;
                        break;
                    }

                    for (count = 1, sibling = t; !!(sibling = sibling.previousSibling); ) {
                        if (sibling.tagName == t.tagName) {
                            ++count;
                        }
                    }

                    tag = t.tagName.toLowerCase();
                    if (count < 2) {
                        xpath.unshift(tag);
                    } else {
                        xpath.unshift(tag + '[' + count + ']');
                    }
                }

                return good ? xpath.join('/') : null;
            },

            getRecordedEvents: function () {
                return this.eventsRecorded;
            },

            // DOMNodeInserted
            onDomInsert: function (event, target) {
                this.watchTree(target);
            },

            //DOMNodeRemoved
            onDomRemove: function (event, target) {
                this.unwatchTree(target);
            },

            onEvent: function (e) {
                var me = this,
                    info = me.eventsToRecord[e.type],
                    root,
                    modKeys, elXY,
                    rec = {
                        type: e.type,
                        ts: me.getTimestamp(),
                        target: me.getElementXPath(e.target)
                    },
                    xy;

                if (!rec.target) {
                    return;
                }
                root = e.target.ownerDocument;
                root = root.defaultView || root.parentWindow; // Standards || IE
                if (root !== me.attachTo) {
                    return;
                }

                if (info.xy) {
                    xy = e.getXY();
                    if (rec.target) {
                        elXY = Ext.fly(e.getTarget()).getXY();
                        xy[0] -= elXY[0];
                        xy[1] -= elXY[1];
                    }
                    rec.x = xy[0];
                    rec.y = xy[1];
                }

                if (info.button) {
                    rec.button = e.button;
                }

                if (info.wheel) {
                    rec.wheel = e.getWheelDelta();
                }

                if (info.modKeys) {
                    me.modKeys[0] = e.altKey ? 'A' : '';
                    me.modKeys[1] = e.ctrlKey ? 'C' : '';
                    me.modKeys[2] = e.metaKey ? 'M' : '';
                    me.modKeys[3] = e.shiftKey ? 'S' : '';

                    modKeys = me.modKeys.join('');
                    if (modKeys) {
                        rec.modKeys = modKeys;
                    }
                }

                if (info.key) {
                    rec.charCode = e.getCharCode();
                    rec.keyCode = e.getKey();
                }

                if (me.coalesce(rec)) {
                    me.fireEvent('coalesce', me, rec);
                } else {
                    me.eventsRecorded.push(rec);
                    me.fireEvent('add', me, rec);
                }
            },

            onStart: function () {
                var me = this,
                    on = {
                        DOMNodeInserted: me.onDomInsert,
                        DOMNodeRemoved: me.onDomRemove,
                        scope: me
                    },
                    nonBubbleEvents = (me.nonBubbleEvents = {}),
                    ddm = me.attachTo.Ext.dd.DragDropManager,
                    evproto = me.attachTo.Ext.EventObjectImpl.prototype;

                me.watchingNodes = {};

                Ext.Object.each(me.eventsToRecord, function (name, value) {
                    if (value) {
                        if (value.bubbles) {
                            on[name] = me.onEvent;
                        } else {
                            nonBubbleEvents[name] = value;
                        }
                    }
                });

                me.ddmStopEvent = ddm.stopEvent;
                ddm.stopEvent = Ext.Function.createSequence(ddm.stopEvent, function (e) {
                    me.onEvent(e);
                });
                me.evStopEvent = evproto.stopEvent;
                evproto.stopEvent = Ext.Function.createSequence(evproto.stopEvent, function () {
                    me.onEvent(this);
                });

                var body = me.attachTo.Ext.getBody();
                body.on(on);
                me.watchTree(body.dom);
            },

            onStop: function () {
                var me = this,
                    body = me.attachTo.Ext.getBody();

                Ext.Object.each(me.eventsToRecord, function (name, value) {
                    if (value) {
                        body.un(name, me.onEvent, me);
                    }
                });

                me.attachTo.Ext.dd.DragDropManager.stopEvent = me.ddmStopEvent;
                me.attachTo.Ext.EventObjectImpl.prototype.stopEvent = me.evStopEvent;

                me.unwatchTree(body.dom);
            },

            watchTree: function (root) {
                if (root.nodeType != 1) {
                    return; // only ELEMENT_NODE's please...
                }

                var me = this,
                    id = (root.tagName == 'BODY') ? '$' : root.id,
                    watchingNodes = me.watchingNodes;

                if (id && !watchingNodes[id]) {
                    var on = {
                        scope: me
                    };

                    Ext.Object.each(me.nonBubbleEvents, function (name, value) {
                        if (value) {
                            on[name] = me.onEvent;
                        }
                    });
                    me.attachTo.Ext.fly(root).on(on);
                    watchingNodes[id] = true;
                    console.log('watch '+root.tagName+'#'+id);
                }

                Ext.each(root.childNodes, me.watchTree, me);
            },

            unwatchTree: function (root) {
                if (root.nodeType != 1) {
                    return; // only ELEMENT_NODE's please...
                }

                var me = this,
                    id = (root.tagName == 'BODY') ? '$' : root.id,
                    watchingNodes = me.watchingNodes;

                if (id && !watchingNodes[id]) {
                    Ext.Object.each(me.nonBubbleEvents, function (name, value) {
                        me.attachTo.Ext.fly(root).un(name, me.onEvent, me);
                    });
                    delete watchingNodes[id];
                    console.log('unwatch '+root.tagName+'#'+id);
                }

                Ext.each(root.childNodes, me.unwatchTree, me);
            }
        };
    }());