define(function(require, module, exports) {
return function(apf) {
var $setTimeout = setTimeout;
var $setInterval = setInterval;

apf.popup = {
    cache: {},
    focusFix: { "INPUT": 1, "TEXTAREA": 1, "SELECT": 1 },
    
    setContent: function(cacheId, content, style, width, height) {
        if (!this.popup) this.init();

        this.cache[cacheId] = {
            content: content,
            style: style,
            width: width,
            height: height
        };
        content.style.position = "absolute";
        
        return content.ownerDocument;
    },
    
    removeContent: function(cacheId) {
        this.cache[cacheId] = null;
        delete this.cache[cacheId];
    },
    
    init: function() {
        //consider using iframe
        this.popup = {};
        
        apf.addEventListener("hotkey", function(e) {
            if (e.keyCode == "27" || e.altKey) 
                apf.popup.forceHide();
        });
    },
    
    show: function(cacheId, options) {
        if (!this.popup) this.init();
        
        options = apf.extend({
            x: 0,
            y: 0,
            animate: false,
            ref: null,
            width: null,
            height: null,
            callback: null,
            draggable: false,
            resizable: false,
            allowTogether: false,
            autoCorrect: true,
            noleft: false,
            setZindex: true
        }, options);
        
        if ((!options.allowTogether 
          || options.allowTogether !== true && options.allowTogether != this.last) 
          && this.last != cacheId
          && this.cache[this.last]
          && (!this.cache[this.last].options || this.cache[this.last].options.autohide !== false))
            this.hide();

        var o = this.cache[cacheId];
        o.options = options;

        var dp,
            popup = o.content,
            moveUp = false,
            moveLeft = false,
            fixed = false;

        if (options.setZindex)
            apf.window.zManager.set(options.zindextype || "popup", o.content);
        
        if ((dp = o.content.style.display) && dp.indexOf("none") > -1)
            o.content.style.display = "";

        var x = options.x;
        var y = options.y;

        var refNode = options.ref;
        while (refNode && refNode.nodeType == 1) {
            if (fixed = apf.getStyle(refNode, "position") == "fixed")
                break;
            refNode = refNode.parentNode || refNode.$parentNode;
        }

        if (!fixed) {
            if (refNode) {
                var pos = apf.getAbsolutePosition(options.ref, 
                    o.content.offsetParent || o.content.parentNode);
                x = (x || 0) + pos[0];
                y = (y || 0) + pos[1];
            }
            
            if (options.width || o.width)
                popup.style.width = ((options.width || o.width) - 3) + "px";

            popup.style.position = "absolute";
            popup.style.maxHeight = "";
            
            var parentMenu = this.cache[options.allowTogether];
            var pOverflow = apf.getOverflowParent(o.content);
            var edgeY = (pOverflow == document.documentElement
                ? window.innerHeight + window.pageYOffset + pOverflow.scrollTop
                : pOverflow.offsetHeight + pOverflow.scrollTop);
            moveUp = options.up || options.autoCorrect && (y
                + (options.height || o.height || o.content.offsetHeight))
                > edgeY;
            
            var maxHeight = 0;
            if (moveUp) {
                var value;
                var height = (options.height || o.height || o.content.offsetHeight);
                if (options.ref)
                    value = (pos[1] - height);
                else
                    value = Math.max(0, edgeY - height);
                
                if (!options.up && value < 0) {
                    moveUp = false;
                    popup.style.top = y + "px";
                    maxHeight = edgeY - y - this.$screenMargin.bottom - 10;
                }
                else {
                    var minTop = this.$screenMargin.top + 3;
                    maxHeight = (pos ? pos[1] : edgeY) - minTop - 10;
                    popup.style.top = Math.max(value, minTop) + "px";
                }
                
            }
            else {
                popup.style.top = y + "px";
                maxHeight = edgeY - y - this.$screenMargin.bottom - 10;
            }
            
            popup.style.overflowY = "auto";
            popup.style.maxHeight = maxHeight ? maxHeight + "px" : "";
            
            if (!options.noleft) {
                var edgeX = (pOverflow == document.documentElement
                    ? window.innerWidth + window.pageXOffset + pOverflow.scrollLeft
                    : pOverflow.offsetWidth + pOverflow.scrollLeft);
                moveLeft = options.autoCorrect && (x
                    + (options.width || o.width || o.content.offsetWidth))
                    > edgeX;

                if (moveLeft) {
                    var value;
                    if (options.ref) {
                        value = (pos[0] - (options.width || o.width || o.content.offsetWidth))
                                + (options.ref.offsetWidth);
                    }
                    else {
                        value = (edgeX - (options.width || o.width || o.content.offsetWidth) 
                                - (parentMenu ? (edgeX - parentMenu.content.offsetLeft) : 0))
                                + 5;
                                //parentMenu.width || parentMenu.content.offsetWidth) : 0));
                    }
                    popup.style.left = value < 0 ? x : (value - 1) + "px";
                }
                else {
                    popup.style.left = x + "px";
                }
            }
        }
        else {
            pos = apf.getAbsolutePosition(options.ref, refNode);
            y = (y || 0) + pos[1] + refNode.offsetTop;
            pos[0] += refNode.offsetLeft;
            popup.style.position = "fixed";
            popup.style.top = y + "px";
            
            if (!options.noleft)
                popup.style.left = x + "px";
        }

        
        // set a className that specifies the direction, to help skins with
        // specific styling options.
        apf.setStyleClass(popup, moveUp ? "upward" : "downward", [moveUp ? "downward" : "upward"]);
        apf.setStyleClass(popup, moveLeft ? "moveleft" : "moveright", [moveLeft ? "moveright" : "moveleft"]);
        

        if (options.animate) {
            if (options.animate == "fade") {
                apf.tween.single(popup, {
                    type: 'fade',
                    from: 0,
                    to: 1,
                    anim: apf.tween.NORMAL,
                    steps: options.steps || 15 * apf.animSteps
                });
            }
            else {
                var iVal, steps = 7, i = 0;
                iVal = setInterval(function() {
                    var value = ++i * ((options.height || o.height) / steps);

                    popup.style.height = value + "px";
                    if (moveUp)
                        popup.style.top = (y - value - (options.y || 0)) + "px";
                    else
                        (options.container || popup).scrollTop = -1 * (i - steps) * ((options.height || o.height) / steps);
                    popup.style.display = "block";

                    if (i >= steps) {
                        clearInterval(iVal);
                        
                        if (options.callback)
                            options.callback(popup);
                    }
                }, 10);
            }
        }
        else {
            if (!refNode) {
                if (options.height || o.height)
                    popup.style.height = (options.height || o.height) + "px";
                value = (edgeY - (options.height || o.height || o.content.offsetHeight));
                popup.style.top = y + (options.height || o.height || o.content.offsetHeight) < edgeY 
                                    ? y 
                                    : value 
                                  + "px";
            }
            popup.style.display = "block";
            
            if (options.callback)
               options.callback(popup);
        }

        $setTimeout(function() {
            apf.popup.last = cacheId;
        });

        if (options.draggable) {
            options.id = cacheId;
            this.makeDraggable(options);
        }
    },
    
    hide: function() {
        if (this.isDragging) return;

        var o = this.cache[this.last];
        if (o) {
            if (o.content)
                o.content.style.display = "none";

            if (o.options && o.options.onclose) {
                o.options.onclose(apf.extend(o.options, { htmlNode: o.content }));
                o.options.onclose = false;
            }
        }
    },
    
    isShowing: function(cacheId) {
        return this.last && this.last == cacheId 
            && this.cache[this.last]
            && this.cache[this.last].content.style.display != "none";
    },

    isDragging: false,

    makeDraggable: function(options) {
        if (!apf.Interactive || this.cache[options.id].draggable) 
            return;

        var oHtml = this.cache[options.id].content;
        this.cache[options.id].draggable = true;
        var o = {
            $propHandlers: {},
            minwidth: 10,
            minheight: 10,
            maxwidth: 10000,
            maxheight: 10000,
            dragOutline: false,
            resizeOutline: false,
            draggable: true,
            resizable: options.resizable,
            $ext: oHtml,
            oDrag: oHtml.firstChild
        };

        oHtml.onmousedown =
        oHtml.firstChild.onmousedown = function(e) {
            if (!e) e = event;
            
            
            
            (e || event).cancelBubble = true;
        };

        apf.implement.call(o, apf.Interactive);

        o.$propHandlers["draggable"].call(o, true);
        o.$propHandlers["resizable"].call(o, true);
    },
    
    getCurrentElement: function() {
        return typeof this.last == "number" && apf.lookup(this.last);
    },
    
    $mousedownHandler: function(amlNode, e) {
        if (!this.last || (amlNode && this.last == amlNode.$uniqueId) || !this.cache[this.last])
          return;

        var htmlNode = e.srcElement || e.target;
        
        var uId = this.last;
        
        while (this.cache[uId]) {
            if (apf.isChildOf(this.cache[uId].content, htmlNode, true))
                return;
            
            if (!this.cache[uId].options)
                return;
            
            uId = this.cache[uId].options.allowTogether;
        }
        
        this.forceHide();
    },
    
    forceHide: function() {
        if (document.body.classList.contains("noInput")) return;
        
        if (this.last 
          
          && !apf.plane.current
          
          && this.isShowing(this.last)
          && this.cache[this.last]
          && this.cache[this.last].options
          && this.cache[this.last].options.autohide !== false) {
            var o = apf.lookup(this.last);
            if (!o)
                this.last = null;
            else if (o.dispatchEvent("popuphide") !== false)
                this.hide();
        }
    },

    destroy: function() {
        for (var cacheId in this.cache) {
            if (this.cache[cacheId]) {
                this.cache[cacheId].content.onmousedown = null;
                apf.destroyHtmlNode(this.cache[cacheId].content);
                this.cache[cacheId].content = null;
                this.cache[cacheId] = null;
            }
        }
        
        if (!this.popup) return;
        //this.popup.document.body.c = null;
        //this.popup.document.body.onmouseover = null;
    },
    
    setMargin: function(m) {
        for (var i in this.$screenMargin)
            if (i in m)
                this.$screenMargin[i] = m[i] || 0;
    },
    $screenMargin: { top: 0, left: 0, right: 0, bottom: 0 }
};






/**
 * This element displays a skinnable menu of items which can be choosen.
 * 
 * Based on the context of the menu, items can be shown and hidden. 
 * 
 *
 * #### Example
 * 
 * ```xml, demo
 *  <a:application xmlns:a="http://ajax.org/2005/aml">
 *   <!-- startcontent -->
 *   <a:menu id="menu1">
 *       <a:item>Tutorials</a:item>
 *       <a:item icon="email.png">Contact</a:item>
 *       <a:divider></a:divider>
 *       <a:item 
 *         icon    = "application_view_icons.png"
 *         hotkey  = "Ctrl+T"
 *         onclick = "setTimeout(function(){alert('You did it');}, 1000)">
 *         Tutorials</a:item>
 *       <a:divider />
 *       <a:item disabled="true">Visit Ajax.org</a:item>
 *       <a:item>Exit</a:item>
 *   </a:menu>
 *   <a:window
 *     width     = "400"
 *     height    = "150"
 *     visible   = "true"
 *     resizable = "true"
 *     title     = "Mail message"
 *     skin      = "bk-window2">
 *       <a:toolbar>
 *           <a:menubar>
 *               <a:button submenu="menu1">File</a:button>
 *               <a:button submenu="menu1" disabled="true">Edit</a:button>
 *           </a:menubar>
 *       </a:toolbar>
 *   </a:window>
 *   <!-- endcontent -->
 *  </a:application>
 * ```
 *
 * @class apf.menu
 * @define menu
 * @selection
 * @allowchild item, divider, check, radio
 *
 * @author      Ruben Daniels (ruben AT ajax DOT org)
 * @version     %I%, %G%
 * @since       0.4
 *
 * @inherits apf.Presentation
 */
/**
 * @event display   Fires when the contextmenu is shown.
 */
/**
 * @event itemclick Fires when a user presses the mouse button while over a child of this element.
 * @param {Object} e The standard event object. The following property is available:
 *   - `value` ([[String]]): the value of the clicked element.
 *
 */
apf.menu = function(struct, tagName) {
    this.$init(tagName || "menu", apf.NODE_VISIBLE, struct);
    
    this.animate = true;
};

(function() {
    this.$focussable = apf.MENU;
    this.$positioning = "basic";
    //var _self         = this;
    //var blurring      = false;

    // *** Properties and Attributes *** //
    
    //this.zindex    = 10000000;
    this.visible = false;
    this.matchhide = false;

    this.$booleanProperties["animate"] = true;
    this.$booleanProperties["pinned"] = true;
    this.$booleanProperties["sticky"] = true;
    this.$booleanProperties["matchhide"] = true;
    
    this.$propHandlers["visible"] = function(value, prop, force, nofocus, hideOpener) {
        if (!this.$ext)
            return;
        
        if (value) {
            this.$ext.style.display = "block";
            if (this.opener && this.opener.localName.indexOf('item') > -1)
                this.opener.parentNode.$showingSubMenu = this;
        }
        else {
            this.$ext.style.display = "none";

            var lastFocus = apf.menu.lastFocussed;
            var opener = this.opener;
            //@todo test this with a list being the opener of the menu
            if (lastFocus != this.opener && this.opener && this.opener.$blur)
                this.opener.$blur();

            if (this.opener && this.opener.parentNode && this.opener.parentNode.localName == "menu") {
                if (!this.$hideTree)
                    this.$hideTree = -1;
                this.opener.parentNode.focus();
            }
            
            
            else if (lastFocus) {
                //We're being hidden because some other object gets focus
                if (apf.window.$settingFocus) {
                    if (apf.window.$settingFocus != lastFocus && lastFocus.$blur)
                        lastFocus.$blur();
                    this.$blur();

                    if (apf.window.$settingFocus.localName != "menu") //not menu walking
                        apf.menu.lastFocussed = null;
                }
                //We're being hidden because window looses focus
                
                //We're just being hidden
                else if (this.$hideTree) {
                    if (!this.$hideTree)
                        this.$hideTree = -1;

                    var visTest = (lastFocus.disabled || lastFocus.$ext 
                        && !lastFocus.$ext.offsetHeight) // || !lastFocus.visible
                        && lastFocus != apf.document.documentElement;

                    if (nofocus || visTest) {
                        if (lastFocus.$blur)
                            lastFocus.$blur();
                        this.$blur();
                        apf.document.activeElement = null;

                        if (visTest && apf.window.moveNext() === false)
                            apf.window.$focusRoot();
                    }
                    else {
                        lastFocus.focus(null, null, true);
                    }

                    apf.menu.lastFocussed = null;
                }
            }
            

            clearTimeout(this.$submenuTimer);

            if (this.$showingSubMenu) {
                this.$showingSubMenu.hide();
                this.$showingSubMenu = null;
            }

            if (this.opener && this.opener.$submenu) {
                this.opener.$submenu(true, true);

                //@todo problem with loosing focus when window looses focus
                if (this.$hideTree === true && this.opener
                  && this.opener.parentNode 
                  && this.opener.parentNode.localName == "menu"
                  && this.opener.parentNode.$hideTree != -1) {
                    this.opener.parentNode.$hideTree = true;
                    this.opener.parentNode.hide();
                }
                
                this.opener = null;
            }
            this.$hideTree = null;

            if (this.$selected) {
                apf.setStyleClass(this.$selected.$ext, "", ["hover"]);
                this.$selected = null;
            }
            
            this.dispatchEvent("hide", { opener: opener });
        }
    };

    // *** Public Methods *** //

    var lastFocus;

    /**
     * Shows the menu, optionally within a certain context.
     * @param {Number}     x        The left position of the menu.
     * @param {Number}     y        The top position of the menu.
     * @param {Boolean}    noanim   Whether to animate the showing of this menu.
     * @param {apf.AmlElement} opener   The element that is the context of this menu.
     * @param {XMLElement} xmlNode  The {@link term.datanode data node} that provides data context to the menu child nodes.
     * @see apf.GuiElement@contextmenu
     */
    this.display = function(x, y, noanim, opener, xmlNode, openMenuId, btnWidth) {
        this.opener = opener;
        
        var lastFocus;
        if (!apf.menu.lastFocussed)
            lastFocus = apf.menu.lastFocussed = apf.menu.lastFocussedItem;
        
        //Show / hide Child Nodes Based on XML
        if (xmlNode && !this.disabled) {
            var last, i, node,
                nodes = this.childNodes,
                c = 0,
                l = nodes.length, result;
            for (i = 0; i < l; i++) {
                node = nodes[i];
                if (node.nodeType != 1 || node.localName != "item")
                    continue;
    
                result = !xmlNode || !node.match || (node.cmatch || (node.cmatch = apf.lm.compile(node.match, {
                    xpathmode: 3,
                    injectself: true
                })))(xmlNode);
    
                if (result) {
                    if (this.matchhide)
                        node.show();
                    else
                        node.enable();
    
                    if (node.localName == "divider" && this.matchhide) {
                        last = node;
                        if (c == 0)
                            node.hide();
                        c = 0;
                    }
                    else c++;
                }
                else {
                    if (this.matchhide)
                        node.hide();
                    else
                        node.disable();
    
                    if (!node.nextSibling && c == 0 && last)
                        last.hide();
                }
            }
        }

        if (this.oOverlay) {
            if (btnWidth) {
                this.oOverlay.style.display = "block";
                this.oOverlay.style.width = btnWidth + "px";
            }
            else
                this.oOverlay.style.display = "none";
        }

        function afterRender() {
            if (x === null) {
                apf.popup.show(this.$uniqueId, {
                    x: 0, 
                    y: this.ref ? 0 : opener.$ext.offsetHeight, 
                    animate: noanim || !this.animate ? false : "fade",
                    steps: 10,
                    ref: (this.ref || opener).$ext,
                    allowTogether: openMenuId,
                    autohide: !this.pinned,
                    noleft: this.left !== undefined,
                    setZindex: this.zindex ? false : true,
                    up: (this.ref || opener).submenudir == "up"
                });
            }
            else {
                //var bodyPos = apf.getAbsolutePosition(document.body);
                apf.popup.show(this.$uniqueId, {
                    x: x, 
                    y: y, 
                    animate: noanim || !this.animate ? false : "fade",
                    steps: 10,
                    //ref          : this.$ext.offsetParent,
                    allowTogether: openMenuId,
                    autohide: !this.pinned,
                    setZindex: this.zindex ? false : true
                    //autoCorrect  : false
                });
            }
            
            // var lastFocus      =
            // apf.menu.lastFocus = opener && opener.$focussable === true
            //     ? opener
            //     : apf.menu.lastFocus || apf.document.activeElement;
            
            apf.popup.last = null;
            
            //if (!apf.isGecko) //This disables keyboard support for gecko - very strange behaviour
                this.focus();
    
            //Make the component that provides context appear to have focus
            // second argument is needed for ace tree
            if (lastFocus && lastFocus != this && lastFocus.$focus)
                lastFocus.$focus(null, { fromContextMenu: true });
    
            this.xmlReference = xmlNode;

            //@todo consider renaming this to onshow and onhide
            this.dispatchEvent("display", { opener: opener });
        }
        
        this.visible = false;
        
        if (!this.parentNode)
            apf.document.documentElement.appendChild(this);
        
        if (this.$rendered !== false) {
            this.show();
            afterRender.call(this);
        }
        else {
            this.addEventListener("afterrender", afterRender);
            this.show();
        }                
    };

    /**
     * Returns the current group value of this element.
     * @return {String} The current selected value.
     */
    this.getValue = function(group) {
        return this.getSelected(group).value || "";
    };

    /**
     * Retrieves the selected element from a group of radio elements.
     * @param {String} group The name of the group.
     * @return {apf.radiobutton} The selected radio element.
     */
    this.getSelected = function(group) {
        var nodes = this.childNodes;
        var i, l = nodes.length;
        for (i = 0; i < l; i++) {
            if (nodes[i].group != group)
                continue;

            if (nodes[i].selected)
                return nodes[i];
        }

        return false;
    };

    /**
     * Selects an element within a radio group.
     * @param {String} group  The name of the group.
     * @param {String} value  The value of the item to select.
     */
    this.select = function(group, value) {
        this.selectedValue = value;
        
        var nodes = this.childNodes;
        var i, l = nodes.length;
        for (i = 0; i < l; i++) {
            if (nodes[i].group != group)
                continue;

            if (value && (nodes[i].value == value || !nodes[i].value && nodes[i].caption == value))
                nodes[i].setProperty("selected", true, false, true);
                //nodes[i].$handlePropSet("selected", true);
            else if (nodes[i].selected)
                nodes[i].setProperty("selected", false, false, true);
                //nodes[i].$handlePropSet("selected", false);
        }
    };

    // *** Events *** //

    
    this.addEventListener("prop.visible", function() {
        this.$initChildren();
    }, true);
    
    this.addEventListener("keydown", function(e) {
        var node, key = e.keyCode;
        //var ctrlKey  = e.ctrlKey;
        //var shiftKey = e.shiftKey;

        switch (key) {
            case 13:
                if (!this.$selected)
                    return;

                node = this.$selected;
                node.$down();
                node.$up();
                node.$click();
                break;
            case 27:
                this.hide();
                break;
            case 38:
                //UP
                node = this.$selected && this.$selected.previousSibling
                  || this.lastChild;

                if (node && node.localName == "divider")
                    node = node.previousSibling;

                if (!node)
                    return;

                if (this.$selected)
                    apf.setStyleClass(this.$selected.$ext, "", ["hover"]);

                apf.setStyleClass(node.$ext, "hover");
                this.$selected = node;
                break;
            case 40:
                //DOWN
                node = this.$selected && this.$selected.nextSibling
                  || this.firstChild;

                if (node && node.localName == "divider")
                    node = node.nextSibling;

                if (!node)
                    return;

                if (this.$selected)
                    apf.setStyleClass(this.$selected.$ext, "", ["hover"]);

                apf.setStyleClass(node.$ext, "hover");
                this.$selected = node;
                break;
            case 37:
                //LEFT
                //if (this.$selected && this.$selected.submenu)
                    //this.$selected.$submenu(true, true);

                if (!this.opener)
                    return;

                if (this.opener.localName == "button") {
                    node = this.opener.previousSibling;
                    while (node && !node.submenu)
                        node = node.previousSibling;

                    if (node) {
                        node.dispatchEvent("mouseover");

                        var btnMenu = node.parentNode.menuIsPressed;
                        if (btnMenu) {
                            self[btnMenu.submenu].dispatchEvent("keydown", {
                                keyCode: 40
                            });
                        }
                    }
                }
                else if (this.opener.parentNode.localName == "menu") {
                    //@todo Ahum bad abstraction boundary
                    var op = this.opener;
                    this.hide();
                    apf.setStyleClass(op.$ext, "hover");
                    op.parentNode.$showingSubMenu = null;
                }

                break;
            case 39:
                //RIGHT
                if (this.$selected && this.$selected.submenu) {
                    this.$selected.$submenu(null, true);
                    this.$showingSubMenu.dispatchEvent("keydown", {
                       keyCode: 40
                    });

                    return;
                }

                if (this.opener) {
                    var op = this.opener;
                    while (op && op.parentNode && op.parentNode.localName == "menu")
                        op = op.parentNode.opener;

                    if (op && op.localName == "button") {
                        node = op.nextSibling;
                        while (node && !node.submenu)
                            node = node.nextSibling;

                        if (node) {
                            node.dispatchEvent("mouseover");

                            var btnMenu = node.parentNode.menuIsPressed;
                            if (btnMenu) {
                                (self[btnMenu.submenu] || btnMenu.submenu).dispatchEvent("keydown", {
                                    keyCode: 40
                                });
                            }

                            return;
                        }
                    }
                }

                if (!this.$selected) {
                    arguments.callee.call(this, {
                       keyCode: 40
                    });
                }

                break;
            default:
                return;
        }

        return false;
    }, true);
    

    //Hide menu when it looses focus or when the popup hides itself
    function forceHide(e) {
        if (this.$showingSubMenu || this.pinned
                || apf.isChildOf(e.fromElement, e.toElement)
                || apf.isChildOf(e.toElement, e.fromElement)
                || apf.isChildOf(this, e.toElement) 
                || (e.name !== "popuphide" && !e.toElement)
                || e.toElement && apf.popup.cache[e.toElement.$uniqueId])
            return;

        if (this.$hideTree != -1) {
            this.$hideTree = true;
            this.hide();
        }

        return false;
    }

    this.addEventListener("focus", function(e) {
        apf.popup.last = this.$uniqueId;
        
        if (!apf.menu.lastFocussed)
            apf.menu.lastFocussed = apf.menu.lastFocussedItem;
    });

    this.addEventListener("blur", forceHide);
    this.addEventListener("popuphide", forceHide);

    // *** Init *** //

    this.$draw = function() {
        this.$pHtmlNode = document.body;

        //Build Main Skin
        this.$ext = this.$getExternal();
        this.oOverlay = this.$getLayoutNode("main", "overlay", this.$ext);

        apf.popup.setContent(this.$uniqueId, this.$ext, "", null, null);
        
        // workaround for a chrome bug where clicking on shadow clciks on contents of overflown element
        this.$ext.addEventListener("mouseup", function(e) {
            var rect = this.getBoundingClientRect();
            if (e.clientY > rect.bottom && rect.height) {
                e.stopPropagation();
                e.preventDefault();
            }
        }, true);
    };

    this.$loadAml = function(x) {
        this.$int = this.$getLayoutNode("main", "container", this.$ext);
    };

    this.$destroy = function() {
        apf.popup.removeContent(this.$uniqueId);
    };
    
    this.$initChildren = function() {
        var ch = this.childNodes;
        for (var i = 0; i < ch.length; i++) {
            var amlNode = ch[i];
            if (!amlNode.$amlLoaded)
                amlNode.dispatchEvent("DOMNodeInsertedIntoDocument");
            // sometimes DOMNodeInsertedIntoDocument event handler puts $ext at the end of the popup
            if (!amlNode.previousSibling || !amlNode.previousSibling.$ext || !amlNode.$ext)
                continue;
            if (amlNode.$ext.previousSibling == amlNode.previousSibling.$ext)
                continue;
            if (amlNode.$ext.parentNode == amlNode.previousSibling.$ext.parentNode)
                amlNode.$ext.parentNode.insertBefore(amlNode.$ext, amlNode.previousSibling.$ext.nextSibling);
        }
    };
    var insertBefore = this.insertBefore;
    this.insertBefore = function(node, beforeNode) {
        // if menu is visible call apf insertBefore since it rearranges html nodes
        // otherwise use fake and much faster method
        if (this.visible)
            return insertBefore.call(this, node, beforeNode);
        if (beforeNode == node)
            return node;
        if (!this || this == node)
            throw new Error("Invalid insertBefore call");
        
        var children = this.childNodes;
        // if (node.parentNode == this)
        //     children[index]
        if (node.parentNode)
            node.removeNode();
        
        var index = beforeNode ? children.indexOf(beforeNode) : children.length;
        node.parentNode = this;
        
        if (beforeNode) {
            children.splice(index, 0, node);
        } else {
            children.push(node);
        }
        
        node.previousSibling = children[index - 1];
        node.nextSibling = children[index + 1];
        if (node.previousSibling)
            node.previousSibling.nextSibling = node;
        else
            this.firstChild = children[0];
            
        if (node.nextSibling)
            node.nextSibling.previousSibling = node;
        else
            this.lastChild = children[this.childNodes.length - 1];
        
        return node;
    };
    
    this.appendChild = function(node) {
        return this.insertBefore(node);
    };
}).call(apf.menu.prototype = new apf.Presentation());

apf.addEventListener("movefocus", function(e) {
    var next = e.toElement;
    if (next && next.localName != "menu")
        apf.menu.lastFocussedItem = next;
});

apf.aml.setElement("menu", apf.menu);



/**
 * Element displaying a divider. For use in toolbars, menus, and such.
 * @class apf.divider
 * @define divider
 * @inherits apf.Presentation
 */
apf.divider = function(struct, tagName) {
    this.$init(tagName || "divider", apf.NODE_VISIBLE, struct);
};

(function() {
    this.$focussable = false;

    this.minwidth = 0;
    this.minheight = 0;

    this.$childProperty = "caption";
    
    //@todo apf3.0 fix this
    this.addEventListener("AMLReparent", function(beforeNode, pNode, withinParent) {
        if (!this.$amlLoaded)
            return;
        
        if (!withinParent && this.skinName != pNode.skinName) {
            //@todo for now, assuming dom garbage collection doesn't leak
            this.loadAml();
        }
    });
    
    /** 
     * @attribute {String} caption the text displayed in the area defined by this 
     * element. 
     */
    this.$supportedProperties.push("caption", "value", "for", "textalign");
    this.$propHandlers["caption"] = function(value) {
        if (this.$caption) {
            this.$setStyleClass(this.$ext, this.$baseCSSname + "Caption");
            this.$caption.textContent = value;
        }
        else {
            this.$setStyleClass(this.$ext, "", [this.$baseCSSname + "Caption"]);
        }
    };
    
    this.$canLeechSkin = true;
    
    /**
     * @private
     */
    this.$draw = function() {
        if (this.parentNode.isPaged && this.parentNode.$buttons)
            this.$pHtmlNode = this.parentNode.$buttons;
        
        if (this.$isLeechingSkin) {
            this.$ext = apf.insertHtmlNode(
                this.parentNode.$getLayoutNode("divider"), this.$pHtmlNode);
        }
        else {
            this.$ext = this.$getExternal("main");
            this.$caption = this.$getLayoutNode("main", "caption", this.$ext);
        }
    };
}).call(apf.divider.prototype = new apf.Presentation());

apf.aml.setElement("divider", apf.divider);






/**
 * Represents an item in a menu, displaying a clickable area.
 * 
 * #### Example
 * 
 * ```xml, demo
 * <a:application xmlns:a="http://ajax.org/2005/aml">
 *   <!-- startcontent -->
 *   <a:menu id="menu1">
 *      <a:item>Tutorials</a:item>
 *      <a:item>Contact</a:item>
 *   </a:menu>
 *   <a:toolbar>
 *      <a:menubar>
 *          <a:button submenu="menu1">File</a:button>
 *      </a:menubar>
 *   </a:toolbar>
 *   <!-- endcontent -->
 * </a:application>
 * ```
 * 
 * @class apf.item
 * @selection
 * @define item
 * @inherits apf.Presentation
 *
 */
/**
 * @event click Fires when a user presses the mouse button while over this element.
 * @param {Object} e The standard event object. It contains the following properties:
 *  - xmlContext ([[XMLElement]]): The XML data node that was selected in the opener at the time of showing the context menu.
 *  - opener ([[apf.AmlElement]]): The element that was clicked upon when showing the context menu.
 */
apf.item = function(struct, tagName) {
    this.$init(tagName || "item", apf.NODE_VISIBLE, struct);
};

(function() {
    this.$focussable = false;
    
    this.$childProperty = "caption";
    this.$canLeechSkin = "item";

    this.checked = false;
    this.selected = false;

    // *** Properties and Attributes *** //
    
    this.$booleanProperties["checked"] = true;
    this.$booleanProperties["selected"] = true;

    this.$supportedProperties.push("submenu", "value", "match", "group", "icon",
                                   "checked", "selected", "disabled", "caption", 
                                   "type", "values");

    /**
     * @attribute {String} submenu Sets or gets the id of the menu that is shown
     * when the user hovers over this menu item.
     * 
     * #### Example
     * 
     * ```xml
     *  <a:menu id="msub">
     *      <a:item icon="tbicons:12">test</a:item>
     *      <a:item icon="tbicons:14">test2</a:item>
     *  </a:menu>
     *
     *  <a:menu id="mmain">
     *      <a:item submenu="msub">Sub menu</a:item>
     *  </a:menu>
     *  
     *  <a:toolbar>
     *      <a:menubar>
     *          <a:button submenu="mmain">File</a:button>
     *      </a:menubar>
     *  </a:toolbar>
     * ```
     */
    this.$propHandlers["submenu"] = function(value) {
        apf.setStyleClass(this.$ext, "submenu");
    };
    
    /**
     * @attribute {String} value Sets or gets the value of this element.
     */

    /**
     * @attribute {String} [select] Sets or gets the XPath statement which works on the
     * XML context of the parent menu element to determine whether this
     * item is shown.
     * 
     * #### Example
     * 
     * This example shows a list:
     * 
     * ```xml
     *   <a:menu id="mnuTest">
     *       <a:item match="[person]" method="send">Send an E-mail</a:item>
     *       <a:item match="[phone]" method="call">Call Number</a:item>
     *       <a:divider />
     *       <a:item match="[phone]" method="remove">Remove</a:item>
     *       <a:divider />
     *       <a:item match="[person|phone]" method="viewpictures">View Pictures</a:item>
     *   </a:menu>
     *   
     *   <a:menu id="mnuXY">
     *       <a:item method="reboot">Reboot</a:item>
     *   </a:menu>
     *   
     *   <a:text contextmenu="mnuXY" width="200" height="200">
     *       Please right-click on this plane
     *   </a:text>
     *   
     *   <a:list id="lstTest" allow-deselect="true" width="200" height="200">
     *       <a:each match="[person|phone|computer]">
     *           <a:caption match="[@caption]" />
     *           <a:icon match="[person]" value="user.png" />
     *           <a:icon match="[phone]" value="phone.png" />
     *           <a:icon match="[computer]" value="computer.png" />
     *       </a:each>
     *       <a:model>
     *           <data>
     *               <person caption="Ruben Daniels" />
     *               <person caption="Rik Arends" />
     *               <phone caption="+31 555 544486" />
     *               <phone caption="+1 555 2392" />
     *               <computer caption="Mail Server" />
     *               <computer caption="File Server" />
     *           </data>
     *       </a:model>
     *       <a:contextmenu menu="mnuXY" match="[computer]" />
     *       <a:contextmenu menu="mnuTest" />
     *   </a:list>
     * ```
     */
    this.$propHandlers["select"] = function(value) {
        this.select = value
            ? "self::" + value.split("|").join("|self::")
            : value;
    };
    
    /**
     * @attribute {String} [group] Sets or gets the name of the group this item belongs
     * to.
     * 
     * #### Example
     * 
     * ```xml
     *  <a:menu>
     *      <a:item type="radio" group="example">item 1</a:item>
     *      <a:item type="radio" group="example">item 2</a:item>
     *      <a:item type="radio" group="example">item 3</a:item>
     *      <a:item type="radio" group="example">item 4</a:item>
     *  </a:menu>
     * ```
     */
    this.$propHandlers["group"] = function(value) {
        if (this.$group && this.$group.$removeRadio)
            this.$group.$removeRadio(this);
            
        if (!value) {
            this.$group = null;
            return;
        }
        
        var group = typeof value == "string"
            ? 
            
            apf.nameserver.get("group", value)
            
            : value;
        if (!group) {
            
            group = apf.nameserver.register("group", value, 
                new apf.$group());
            group.setAttribute("id", value);
            group.dispatchEvent("DOMNodeInsertedIntoDocument");
            group.parentNode = this;
            
        }
        this.$group = group;
        
        this.$group.$addRadio(this);
    };

    
    /**
     * @attribute {String} hotkey Sets or gets the key combination a user can press
     * to active the function of this element. Use any combination of
     * [[keys: Ctrl]], [[keys: Shift]], [[keys: Alt]], [[keys: F1]]-[[keys: F12]], and alphanumerical characters. Use a
     * space, a minus or plus sign as a seperator.
     * 
     * #### Example
     * 
     * ```xml
     *  <a:item hotkey="Ctrl+Q">Quit</a:item>
     * ```
     */
    this.$propHandlers["hotkey"] = function(value) {
        
    };
    
    /**
     * @attribute {String} icon Sets or gets the URL of the image used as an icon or
     * a reference to an iconmap.
     */
    this.$propHandlers["icon"] = function(value) {
        if (this.$icon)
            apf.skins.setIcon(this.$icon, value, this.parentNode.iconPath);
    };
    
    /**
     * @attribute {String} caption Sets or gets the text displayed on the item.
     */
    this.$propHandlers["caption"] = function(value) {
        if (this.$caption)
            apf.setNodeValue(this.$caption, value);
    };
    
    /**
     * @attribute {String} type Sets or gets the function of this item.
     * 
     * Possible values include:
     * - `"item"`
     * - `"check"`
     * - `"radio"`
     */
    this.$propHandlers["type"] = function(value) {
        apf.setStyleClass(this.$ext, value, ["item", "check", "radio"]);
    };
    
    this.$propHandlers["values"] = function(value) {
        this.$values = typeof value == "string"
            ? value.split("\|")
            : (value || [1, 0]);

        this.$propHandlers["value"].call(this, this.value);
    };
    
    this.$propHandlers["value"] = function(value) {
        if (this.type != "check")
            return;
        
        value = (typeof value == "string" ? value.trim() : value);
        
        var checked;
        if (this.$values) {
            checked = (typeof value != "undefined" && value !== null
                && value.toString() == this.$values[0].toString());
        }
        else {
            checked = apf.isTrue(value);
        }
        
        if (checked != this.checked) {
            this.checked = checked;
            this.$propHandlers.checked.call(this, checked);
        }
    };
    
    /**
     * @attribute {Boolean} checked Sets or gets whether the item is checked.
     */
    this.$propHandlers["checked"] = function(value) {
        if (this.type != "check")
            return;

        if (apf.isTrue(value))
            apf.setStyleClass(this.$ext, "checked");
        else
            apf.setStyleClass(this.$ext, "", ["checked"]);
        
        if (!this.$values) {
            if (this.getAttribute("values"))
                this.$propHandlers["values"].call(this, this.getAttribute("values"));
            else
                this.$values = [true, false];
        }
        
        if (this.$values && this.$values[value ? 0 : 1] != this.value)
            this.setProperty("value", this.$values ? this.$values[value ? 0 : 1] : true);
    };
    
    this.select = function() {
        this.parentNode.select(this.group, this.value != undefined ? this.value : this.caption);
    };
    
    this.check = function() {
        this.setProperty("value", this.$values
            ? this.$values[0]
            : true);
    };
    this.uncheck = function() {
        this.setProperty("value", this.$values
            ? this.$values[1]
            : false);
    };
    
    this.$check = function() {
        apf.setStyleClass(this.$ext, "selected");
    };
    
    this.$uncheck = function() {
        apf.setStyleClass(this.$ext, "", ["selected"]);
    };

    /**
     * @attribute {Boolean} selected Sets or gets whether the item is selected.
     */
    this.$propHandlers["selected"] = function(value) {
        if (this.type != "radio")
            return;


        if (apf.isTrue(value)) {
            if (this.$group)
                this.$group.setProperty("value", this.value);
            this.$check();
        }
        else
            this.$uncheck();
    };
    
    /**
     * @attribute {Boolean} disabled Sets or gets whether the item is active.
     */
    this.$propHandlers["disabled"] = function(value) {
        if (apf.isTrue(value) || value == -1)
            apf.setStyleClass(this.$ext, "disabled");
        else
            apf.setStyleClass(this.$ext, "", ["disabled"]);
    };

    // *** Dom Hooks *** //

    //@todo apf3.0
    this.addEventListener("AMLReparent", function(beforeNode, pNode, withinParent) {
        if (!this.$amlLoaded)
            return;

        if (!withinParent && this.skinName != pNode.skinName) {
            //@todo for now, assuming dom garbage collection doesn't leak
            this.loadAml();
        }
    });

    // *** Events *** //

    this.$down = function() {
    
    };

    this.$up = function(e) {
   
        
        if (this.type == "radio")
            this.parentNode.select(this.group, this.value != undefined ? this.value : this.caption);

        else if (this.type == "check") {
            this.setProperty("checked", !this.checked);
            //this.$handlePropSet("checked", !this.checked);
        }

        if (this.submenu) {
            this.$over(null, true);
            return;
        }

        this.parentNode.$hideTree = true;
        
        //@todo This statement makes the menu loose focus.
        if (!this.parentNode.sticky && this.parentNode.localName != "dropdown")
            this.parentNode.hide();//true not focus?/

        this.parentNode.dispatchEvent("itemclick", {
            value: this.value != undefined ? this.value : this.caption,
            relatedNode: this,
            checked: this.checked,
            selected: this.selected
        });

        //@todo Anim effect here?
        
        this.dispatchEvent("click", {
            xmlContext: (this.parentNode || 0).xmlReference,
            opener: (this.parentNode || 0).opener,
            button: e && e.button
        });
        
        
    };

    this.$click = function() {
        
    };

    var timer;
    this.$out = function(e) {
        if (apf.isChildOf(this.$ext, e.toElement || e.explicitOriginalTarget)
          || apf.isChildOf(this.$ext, e.srcElement || e.target))  //@todo test FF
            return;

        clearTimeout(timer);
        if (!this.submenu || this.$submenu(true)) {
            apf.setStyleClass(this.$ext, "", ['hover']);

            var sel = this.parentNode.$selected;
            if (sel && sel != this)
                apf.setStyleClass(sel.$ext, "", ["hover"]);

            this.parentNode.$selected = null;
        }
        
        
    };

    this.$over = function(e, force) {
        function selectItem(el) {
            if (el.parentNode.$selected == el && e)
                return false;
    
            if (el.parentNode.$selected)
                apf.setStyleClass(el.parentNode.$selected.$ext, "", ["hover"]);
    
            apf.setStyleClass(el.$ext, "hover");
            el.parentNode.$selected = el;
            return true;
        }
        
        if (!selectItem(this))
            return;
        
        var opener = this.parentNode.opener;
        if (opener && opener.parentNode && opener.parentNode.$showingSubMenu)
            selectItem(opener);
        
        if (!force && (apf.isChildOf(this.$ext, e.toElement || e.explicitOriginalTarget)
          || apf.isChildOf(this.$ext, e.fromElement || e.target)))  //@todo test FF
            return;
        
        var _self = this, ps = this.parentNode.$showingSubMenu;
        if (ps && ps.name && ps.name == this.submenu) {
            this.parentNode.$selected = null;
            this.parentNode.$showingSubMenu = null;
            _self.$submenu();
            return;
        }
            
        clearTimeout(timer);
        
        
        function submenu() {
            if (ps && ps.visible) {
                ps.hide();
                
                if (_self.parentNode.$showingSubMenu == ps)
                    _self.parentNode.$showingSubMenu = null;
            }
            if (_self.submenu && (!_self.parentNode.opener 
              || _self.parentNode.opener.visible))
                _self.$submenu();
        }

        if (force)
            submenu();
        else {
            timer = $setTimeout(function() {
                submenu();
                timer = null;
            }, 210);
        }
    };

    this.$submenu = function(hide, force) {
        if (!this.submenu)
            return true;

        var menu = self[this.submenu] || this.submenu;
        if (!menu) {
            

            return;
        }

        if (!hide) {
            //if (this.parentNode.showingSubMenu == this.submenu)
                //return;

            this.parentNode.$showingSubMenu = menu;

            var pos = apf.getAbsolutePosition(this.$ext, this.parentNode.$ext.offsetParent);
            menu.display(pos[0] + this.$ext.offsetWidth - 3,
                pos[1] + 3, true, this,
                this.parentNode.xmlReference, this.parentNode.$uniqueId);
            menu.setAttribute("zindex", (this.parentNode.zindex || this.parentNode.$ext.style.zIndex || 1) + 1);
        }
        else {
            if (menu.visible && !force) {
                return false;
            }
            
            if (this.parentNode.$showingSubMenu == menu)
                this.parentNode.$showingSubMenu = null;
            
            apf.setStyleClass(this.$ext, '', ['hover']);
            menu.hide();
            return true;
        }
    };

    // *** Init *** //
    
    this.$draw = function(isSkinSwitch) {
        var p = this.parentNode;
        while (p.$canLeechSkin == "item")
            p = p.parentNode;
        
        this.$ext = this.$getExternal(this.$isLeechingSkin
          ? "item" //this.type 
          : "main", null, function($ext) {
            var o = 'var o = apf.lookup(' + this.$uniqueId + '); if (!o || o.disabled) return; o';
            $ext.setAttribute("onmouseup", o + '.$up(event)');
            $ext.setAttribute("onmousemove", o + '.$over(event)');
            $ext.setAttribute("onmouseout", o + '.$out(event)');
            $ext.setAttribute("onmousedown", o + '.$down()');
            $ext.setAttribute("onclick", o + '.$click()');
        });
        // getExternal always appends to the end which is wrong when drawing is delayed
        var next = this.nextSibling && this.nextSibling.$ext;
        if (next && next.parentNode === this.$ext.parentNode && this.$ext.nextSibling !== next) {
            next.parentNode.insertBefore(this.$ext, next);
        }

        var _self = this;
        apf.addListener(this.$ext, "mouseover", function(e) {
            if (!_self.disabled)
                _self.dispatchEvent("mouseover", { htmlEvent: e });
        });
        
        apf.addListener(this.$ext, "mouseout", function(e) {
            if (!_self.disabled)
                _self.dispatchEvent("mouseout", { htmlEvent: e });
        });
        
        /*p.$getNewContext("item");
        var elItem = p.$getLayoutNode("item");*/
        
        //@todo if not elItem try using own skin
        
        //this.$ext   = apf.insertHtmlNode(elItem, this.parentNode.$container);
        this.$caption = this.$getLayoutNode("item", "caption", this.$ext);
        this.$icon = this.$getLayoutNode("item", "icon", this.$ext);
        this.$hotkey = this.$getLayoutNode("item", "hotkey", this.$ext);

        if (!isSkinSwitch && this.nextSibling && this.nextSibling.$ext 
            && this.nextSibling.$ext.parentNode == this.$ext.parentNode) {
            this.$ext.parentNode.insertBefore(this.$ext, this.nextSibling.$ext);
        }
    };
    
    /*
     * @private
     */
    this.addEventListener("DOMNodeInsertedIntoDocument", function(e) {
        //var x = this.$aml;

        //this.skinName    = this.parentNode.skinName;
        var isSkinSwitch = this.$ext ? true : false;
        if (isSkinSwitch) {
            if (typeof this.checked !== "undefined")
                this.$handlePropSet("checked", this.checked);
            else if (typeof this.selected !== "undefined")
                this.$handlePropSet("selected", this.selected);

            if (this.disabled)
                this.$handlePropSet("disabled", this.disabled);

            if (this.caption)
                this.$handlePropSet("caption", this.caption);
        }
    });
}).call(apf.item.prototype = new apf.Presentation());

apf.aml.setElement("item", apf.item);

};

});