/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-6 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330, 
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

  ==============================================================================
*/

#include "../../../../juce_core/basics/juce_StandardHeader.h"

BEGIN_JUCE_NAMESPACE

#include "juce_PopupMenu.h"
#include "../lookandfeel/juce_LookAndFeel.h"
#include "../juce_ComponentDeletionWatcher.h"
#include "../juce_Desktop.h"
#include "../../graphics/imaging/juce_Image.h"
#include "../../../events/juce_Timer.h"
#include "../../../../juce_core/threads/juce_Process.h"
#include "../../../../juce_core/basics/juce_Time.h"


//==============================================================================
class MenuItemInfo
{
public:
    //==============================================================================
    const int itemId;
    String text;
    const Colour textColour;
    const bool active, isSeparator, isTicked, usesColour;
    Image* image;
    PopupMenuCustomComponent* const customComp;
    PopupMenu* subMenu;
    KeyPressMappingSet* const keyMappingSetToTrigger;
    const int keyMappingCommandUID;

    //==============================================================================
    MenuItemInfo() throw()
        : itemId (0),
          active (true),
          isSeparator (true),
          isTicked (false),
          usesColour (false),
          image (0),
          customComp (0),
          subMenu (0),
          keyMappingSetToTrigger (0),
          keyMappingCommandUID (0)
    {
    }

    MenuItemInfo (const int itemId_,
                  const String& text_,
                  const bool active_,
                  const bool isTicked_,
                  const Image* im,
                  const Colour& textColour_,
                  const bool usesColour_,
                  PopupMenuCustomComponent* const customComp_,
                  const PopupMenu* const subMenu_,
                  KeyPressMappingSet* const keyMappingSetToTrigger_,
                  const int keyMappingCommandUID_)
        : itemId (itemId_),
          text (text_),
          textColour (textColour_),
          active (active_),
          isSeparator (false),
          isTicked (isTicked_),
          usesColour (usesColour_),
          image (0),
          customComp (customComp_),
          keyMappingSetToTrigger (keyMappingSetToTrigger_),
          keyMappingCommandUID (keyMappingCommandUID_)
    {
        subMenu = (subMenu_ != 0) ? new PopupMenu (*subMenu_) : 0;

        if (customComp != 0)
            customComp->refCount_++;

        if (im != 0)
            image = im->createCopy();

        if (keyMappingSetToTrigger_ != 0 && keyMappingCommandUID_ != 0)
        {
            String shortcutKey;

            OwnedArray <KeyPress> keyPresses;
            keyMappingSetToTrigger_->getKeyPressesAssignedToCommand (keyMappingCommandUID_, keyPresses);

            for (int i = 0; i < keyPresses.size(); ++i)
            {
                const String key (keyPresses.getUnchecked(i)->getTextDescription());

                if (shortcutKey.isNotEmpty())
                    shortcutKey << T(", ");

                if (key.length() == 1)
                    shortcutKey << T("shortcut: '") << key << T("'");
                else
                    shortcutKey << key;
            }

            shortcutKey = shortcutKey.trim();

            if (shortcutKey.isNotEmpty())
                text << T("<end>") << shortcutKey;
        }
    }

    MenuItemInfo (const MenuItemInfo& other)
        : itemId (other.itemId),
          text (other.text),
          textColour (other.textColour),
          active (other.active),
          isSeparator (other.isSeparator),
          isTicked (other.isTicked),
          usesColour (other.usesColour),
          customComp (other.customComp),
          keyMappingSetToTrigger (other.keyMappingSetToTrigger),
          keyMappingCommandUID (other.keyMappingCommandUID)
    {
        if (other.subMenu != 0)
            subMenu = new PopupMenu (*(other.subMenu));
        else
            subMenu = 0;

        if (other.image != 0)
            image = other.image->createCopy();
        else
            image = 0;

        if (customComp != 0)
            customComp->refCount_++;
    }

    ~MenuItemInfo()
    {
        delete subMenu;
        delete image;

        if (customComp != 0 && --(customComp->refCount_) == 0)
            delete customComp;
    }

    bool canBeTriggered() const
    {
        return active && ! (isSeparator || (subMenu != 0));
    }

    bool hasActiveSubMenu() const
    {
        return active && (subMenu != 0);
    }

    juce_UseDebuggingNewOperator

private:
    const MenuItemInfo& operator= (const MenuItemInfo&);
};

//==============================================================================
class MenuItemComponent  : public Component
{
    bool isHighlighted;

public:
    MenuItemInfo itemInfo;

    //==============================================================================
    MenuItemComponent (const MenuItemInfo& itemInfo_)
      : isHighlighted (false),
        itemInfo (itemInfo_)
    {
        if (itemInfo.customComp != 0)
            addAndMakeVisible (itemInfo.customComp);
    }

    ~MenuItemComponent()
    {
        if (itemInfo.customComp != 0)
            removeChildComponent (itemInfo.customComp);
    }

    void getIdealSize (int& idealWidth,
                       int& idealHeight)
    {
        if (itemInfo.customComp != 0)
        {
            itemInfo.customComp->getIdealSize (idealWidth, idealHeight);
        }
        else
        {
            getLookAndFeel().getIdealPopupMenuItemSize (itemInfo.text,
                                                        itemInfo.isSeparator,
                                                        idealWidth,
                                                        idealHeight);
        }
    }

    void paint (Graphics& g)
    {
        if (itemInfo.customComp == 0)
        {
            String mainText (itemInfo.text);
            String endText;
            const int endIndex = mainText.indexOf (T("<end>"));

            if (endIndex >= 0)
            {
                endText = mainText.substring (endIndex + 5).trim();
                mainText = mainText.substring (0, endIndex);
            }

            getLookAndFeel()
                .drawPopupMenuItem (g, getWidth(), getHeight(),
                                    itemInfo.isSeparator,
                                    itemInfo.active,
                                    isHighlighted,
                                    itemInfo.isTicked,
                                    itemInfo.subMenu != 0,
                                    mainText, endText,
                                    itemInfo.image,
                                    itemInfo.usesColour ? &(itemInfo.textColour) : 0);
        }
    }

    void resized()
    {
        if (getNumChildComponents() > 0)
            getChildComponent(0)->setBounds (2, 0, getWidth() - 4, getHeight());
    }

    void setHighlighted (bool shouldBeHighlighted)
    {
        shouldBeHighlighted = shouldBeHighlighted && itemInfo.active;

        if (isHighlighted != shouldBeHighlighted)
        {
            isHighlighted = shouldBeHighlighted;
            repaint();
        }
    }

private:
    MenuItemComponent (const MenuItemComponent&);
    const MenuItemComponent& operator= (const MenuItemComponent&);
};


//==============================================================================
static const int scrollZone = 24;
static const int borderSize = 2;
static const int timerInterval = 50;
static const int maxColumns = 7;

static bool wasHiddenBecauseOfAppChange = false;

//==============================================================================
class PopupMenuWindow  : public Component,
                         public Timer
{
public:
    //==============================================================================
    PopupMenuWindow()
       : Component (T("menu")),
         owner (0),
         currentChild (0),
         activeSubMenu (0),
         menuBarComponent (0),
         lastMouseX (0),
         lastMouseY (0),
         isOver (false),
         hasBeenOver (false),
         isDown (false),
         isScrolling (false),
         hideOnExit (false),
         disableMouseMoves (false),
         numColumns (0),
         totalH (0),
         childYOffset (0)
    {
        lastFocused = Time::getMillisecondCounter();
        setWantsKeyboardFocus (true);

        setOpaque (true);
        setAlwaysOnTop (true);

        Desktop::getInstance().addGlobalMouseListener (this);
    }

    ~PopupMenuWindow()
    {
        Desktop::getInstance().removeGlobalMouseListener (this);

        jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent());
        delete activeSubMenu;

        deleteAllChildren();
    }

    //==============================================================================
    static PopupMenuWindow* create (const PopupMenu& menu,
                                    const bool dismissOnMouseUp,
                                    PopupMenuWindow* const owner_,
                                    const int minX, const int maxX,
                                    const int minY, const int maxY,
                                    const bool alignToRectangle,
                                    Component* const menuBarComponent)
    {
        if (menu.items.size() > 0)
        {
            int totalItems = 0;
            bool lastItemWasSeparator = true;

            PopupMenuWindow* const mw = new PopupMenuWindow();
            mw->setLookAndFeel (menu.lookAndFeel);

            mw->dismissOnMouseUp = dismissOnMouseUp;

            for (int i = 0; i < menu.items.size(); ++i)
            {
                MenuItemInfo* const item = (MenuItemInfo*) menu.items.getUnchecked(i);

                if (item->isSeparator)
                {
                    if (! lastItemWasSeparator)
                    {
                        // check it's not one of the last separators..
                        for (int j = i + 1; j < menu.items.size(); ++j)
                        {
                            if (! ((MenuItemInfo*) menu.items.getUnchecked (j))->isSeparator)
                            {
                                mw->addItem (*item);
                                ++totalItems;

                                break;
                            }
                        }
                    }
                }
                else
                {
                    mw->addItem (*item);
                    ++totalItems;
                }

                lastItemWasSeparator = item->isSeparator;
            }

            if (totalItems == 0)
            {
                delete mw;
            }
            else
            {
                mw->owner = owner_;
                mw->menuBarComponent = menuBarComponent;
                mw->setPosition (minX, maxX, minY, maxY, alignToRectangle);

                mw->addToDesktop (ComponentPeer::windowIsTemporary
                                    | mw->getLookAndFeel().getMenuWindowFlags());

                return mw;
            }
        }

        return 0;
    }

    //==============================================================================
    void paint (Graphics& g)
    {
        getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight());
    }

    void paintOverChildren (Graphics& g)
    {
        if (isScrolling)
        {
            LookAndFeel& lf = getLookAndFeel();
            const int scrollBoxH = scrollZone;

            lf.drawPopupMenuUpDownArrow (g, getWidth(), scrollBoxH, true);

            g.setOrigin (0, getHeight() - scrollBoxH);
            lf.drawPopupMenuUpDownArrow (g, getWidth(), scrollBoxH, false);
        }
    }

    //==============================================================================
    void addItem (const MenuItemInfo& item)
    {
        MenuItemComponent* const mic = new MenuItemComponent (item);
        addAndMakeVisible (mic);

        int itemW = 80;
        int itemH = 16;
        mic->getIdealSize (itemW, itemH);
        mic->setSize (itemW, jlimit (10, 300, itemH));
        mic->addMouseListener (this, false);
    }

    void ensureItemIsVisible (const int itemId)
    {
        if (itemId != 0)
        {
            for (int i = getNumChildComponents(); --i >= 0;)
            {
                MenuItemComponent* const m = (MenuItemComponent*) getChildComponent (i);

                if (m != 0 && m->itemInfo.itemId == itemId)
                {
                    const int currentY = m->getY();
                    const int wantedY = jlimit (scrollZone,
                                                jmax (scrollZone, getHeight() - (scrollZone + m->getHeight())),
                                                currentY);

                    childYOffset -= wantedY - currentY;
                    updateYPositions();

                    break;
                }
            }
        }
    }

    //==============================================================================
    // hide this and all sub-comps
    void hide (const MenuItemInfo* const item)
    {
        if (isVisible())
        {
            jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent());

            deleteAndZero (activeSubMenu);
            currentChild = 0;

            exitModalState (item != 0 ? item->itemId : 0);
            setVisible (false);

            if (item != 0
                 && item->keyMappingSetToTrigger != 0
                 && item->keyMappingCommandUID != 0)
            {
                item->keyMappingSetToTrigger->invokeCommand (item->keyMappingCommandUID, true);
            }
        }
    }

    void dismissMenu (const MenuItemInfo* const item)
    {
        if (owner != 0)
        {
            owner->dismissMenu (item);
        }
        else
        {
            if (item != 0)
            {
                // need a copy of this on the stack as the one passed in will get deleted during this call
                const MenuItemInfo mi (*item);
                hide (&mi);
            }
            else
            {
                hide (0);
            }
        }
    }

    //==============================================================================
    void mouseMove (const MouseEvent&)
    {
        timerCallback();
    }

    void mouseDown (const MouseEvent&)
    {
        timerCallback();
    }

    void mouseDrag (const MouseEvent&)
    {
        timerCallback();
    }

    void mouseUp (const MouseEvent&)
    {
        timerCallback();
    }

    void mouseWheelMove (const MouseEvent&, float amount)
    {
        childYOffset += roundFloatToInt (-10.0f * amount * scrollZone);
        updateYPositions();
        lastMouseX = -1;
    }

    void keyPressed (const KeyPress& key)
    {
        if (key.isKeyCode (KeyPress::downKey))
        {
            selectNextItem (1);
        }
        else if (key.isKeyCode (KeyPress::upKey))
        {
            selectNextItem (-1);
        }
        else if (key.isKeyCode (KeyPress::leftKey))
        {
            PopupMenuWindow* parentWindow = owner;

            if (parentWindow != 0)
            {
                MenuItemComponent* currentChildOfParent
                    = (parentWindow != 0) ? parentWindow->currentChild : 0;

                hide (0);

                if (parentWindow->isValidComponent())
                    parentWindow->setCurrentlyHighlightedChild (currentChildOfParent);

                disableTimerUntilMouseMoves();
            }
            else if (menuBarComponent != 0)
            {
                menuBarComponent->keyPressed (key);
            }
        }
        else if (key.isKeyCode (KeyPress::rightKey))
        {
            disableTimerUntilMouseMoves();

            if (showSubMenuFor (currentChild))
            {
                jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent());

                if (activeSubMenu != 0 && activeSubMenu->isVisible())
                    activeSubMenu->selectNextItem (1);
            }
            else if (menuBarComponent != 0)
            {
                menuBarComponent->keyPressed (key);
            }
        }
        else if (key.isKeyCode (KeyPress::returnKey))
        {
            triggerCurrentlyHighlightedItem();
        }
        else if (key.isKeyCode (KeyPress::escapeKey))
        {
            dismissMenu (0);
        }
    }

    void inputAttemptWhenModal()
    {
        timerCallback();

        if (! isOverAnyMenu())
            dismissMenu (0);
    }

    //==============================================================================
    void timerCallback()
    {
        if (! isVisible())
            return;

        startTimer (timerInterval);  // do this in case it was called from a mouse
                                     // move rather than a real timer callback

        const int mx = getMouseX();
        const int my = getMouseY();
        const int x = mx - getScreenX();
        const int y = my - getScreenY();

        const uint32 now = Time::getMillisecondCounter();

        if (now > timeEnteredCurrentChildComp + 100
             && reallyContains (x, y, true)
             && currentChild->isValidComponent()
             && (! disableMouseMoves)
             && ! (activeSubMenu != 0 && activeSubMenu->isVisible()))
        {
            showSubMenuFor (currentChild);
        }

        if (mx != lastMouseX || my != lastMouseY || now > lastMouseMoveTime + 350)
        {
            highlightItemUnderMouse (mx, my, x, y);
        }

        bool overScrollArea = false;

        if (isScrolling && isOver && (now > lastScroll + 20))
        {
            if (y < scrollZone)
            {
                childYOffset -= scrollZone;
                updateYPositions();
                lastMouseX = -1; // trigger a mouse-move
                overScrollArea = true;
            }
            else if (y > getHeight() - scrollZone)
            {
                childYOffset += scrollZone;
                updateYPositions();
                lastMouseX = -1; // trigger a mouse-move
                overScrollArea = true;
            }

            lastScroll = now;
        }

        const bool wasDown = isDown;

        if (hideOnExit && hasBeenOver && ! isOverAnyMenu())
        {
            hide (0);
        }
        else
        {
            isDown = hasBeenOver
                       && ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown();

            if (Component::getCurrentlyFocusedComponent() == 0
                 || ! Process::isForegroundProcess())
            {
                if (now > lastFocused + 10)
                {
                    wasHiddenBecauseOfAppChange = true;
                    dismissMenu (0);

                    return;  // may have been deleted by the previous call..
                }
            }
            else if (wasDown && ! (isDown || overScrollArea))
            {
                isOver = reallyContains (x, y, true);

                if (isOver)
                {
                    triggerCurrentlyHighlightedItem();
                }
                else if ((hasBeenOver || ! dismissOnMouseUp) && ! isOverAnyMenu())
                {
                    dismissMenu (0);
                }

                return;  // may have been deleted by the previous calls..
            }
            else
            {
                lastFocused = now;
            }
        }
    }

    //==============================================================================
    juce_UseDebuggingNewOperator

private:
    PopupMenuWindow* owner;
    MenuItemComponent* currentChild;
    PopupMenuWindow* activeSubMenu;
    Component* menuBarComponent;
    int lastMouseX, lastMouseY;
    bool isOver, hasBeenOver, isDown, isScrolling;
    bool dismissOnMouseUp, hideOnExit, disableMouseMoves;
    int numColumns, totalH, childYOffset;
    int columnWidths [maxColumns];
    uint32 lastFocused, lastScroll, lastMouseMoveTime, timeEnteredCurrentChildComp;

    //==============================================================================
    bool overlaps (const Rectangle& r) const throw()
    {
        return r.intersects (getBounds())
                || (owner != 0 && owner->overlaps (r));
    }

    bool isOverAnyMenu() const throw()
    {
        return (owner != 0) ? owner->isOverAnyMenu()
                            : isOverChildren();
    }

    bool isOverChildren() const throw()
    {
        jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent());

        return isVisible()
                && (isOver || (activeSubMenu != 0 && activeSubMenu->isOverChildren()));
    }

    //==============================================================================
    void setPosition (const int minX, const int maxX,
                      const int minY, const int maxY,
                      const bool alignToRectangle)
    {
        const Rectangle mon (Desktop::getInstance()
                                .getMonitorAreaContaining ((minX + maxX) / 2,
                                                           (minY + maxY) / 2));

        int x, y, widthToUse, heightToUse;
        layoutMenuItems (mon.getWidth() - 24, widthToUse, heightToUse);

        if (alignToRectangle)
        {
            x = minX;

            const int spaceUnder = mon.getHeight() - maxY;
            const int spaceOver = minY - mon.getY();

            if (heightToUse < spaceUnder - 30 || spaceUnder >= spaceOver)
                y = maxY;
            else
                y = minY - heightToUse;
        }
        else
        {
            bool tendTowardsRight = (minX + maxX) / 2 < mon.getCentreX();

            if (owner != 0)
            {
                if (owner->owner != 0)
                {
                    const bool ownerGoingRight = (owner->getX() + owner->getWidth() / 2
                                                    > owner->owner->getX() + owner->owner->getWidth() / 2);

                    if (ownerGoingRight && maxX + widthToUse < mon.getRight() - 4)
                        tendTowardsRight = true;
                    else if ((! ownerGoingRight) && minX > widthToUse + 4)
                        tendTowardsRight = false;
                }
                else if (maxX + widthToUse < mon.getRight() - 32)
                {
                    tendTowardsRight = true;
                }
            }

            const int biggestSpace = jmax (mon.getRight() - maxX,
                                           minX - mon.getX()) - 32;

            if (biggestSpace < widthToUse)
            {
                layoutMenuItems (biggestSpace + (maxX - minX) / 3, widthToUse, heightToUse);

                if (numColumns > 1)
                    layoutMenuItems (biggestSpace - 4, widthToUse, heightToUse);

                tendTowardsRight = (mon.getRight() - maxX) >= (minX - mon.getX());
            }

            if (tendTowardsRight)
                x = jmin (mon.getRight() - widthToUse - 4, maxX);
            else
                x = jmax (mon.getX() + 4, minX - widthToUse);

            y = minY;
            if ((minY + maxY) / 2 > mon.getCentreY())
                y = jmax (mon.getY(), maxY - heightToUse);
        }

        x = jlimit (mon.getX() + 1, mon.getRight() - (widthToUse + 6), x);
        y = jlimit (mon.getY() + 1, mon.getBottom() - (heightToUse + 6), y);

        setBounds (x, y, widthToUse, heightToUse);

        // sets this flag if it's big enough to obscure any of its parent menus
        Rectangle bounds (getBounds());
        bounds.expand (-4, -4);
        hideOnExit = (owner != 0) && owner->overlaps (bounds);
    }

    void layoutMenuItems (const int maxMenuW, int& width, int& height)
    {
        numColumns = 1;
        totalH = 0;
        const int maxMenuH = getParentHeight() - 24;
        int totalW;

        while (numColumns < maxColumns)
        {
            totalW = workOutBestSize (numColumns, maxMenuW);

            if (totalW > maxMenuW)
            {
                numColumns = jmax (1, numColumns - 1);
                totalW = workOutBestSize (numColumns, maxMenuW); // to update col widths
                break;
            }
            else if (totalW > maxMenuW / 2 || totalH < maxMenuH)
            {
                break;
            }

            ++numColumns;
        }

        const int actualH = jmin (totalH, maxMenuH);

        isScrolling = totalH > maxMenuH;

        width = updateYPositions();
        height = actualH + borderSize * 2;
    }

    int workOutBestSize (const int numColumns, const int maxMenuW)
    {
        int totalW = 0;
        totalH = 0;
        int childNum = 0;

        for (int col = 0; col < numColumns; ++col)
        {
            int i, colW = 50, colH = 0;

            const int numChildren = jmin (getNumChildComponents() - childNum,
                                          (getNumChildComponents() + numColumns - 1) / numColumns);

            for (i = numChildren; --i >= 0;)
            {
                colW = jmax (colW, getChildComponent (childNum + i)->getWidth());
                colH += getChildComponent (childNum + i)->getHeight();
            }

            colW = jmin (maxMenuW / jmax (1, numColumns - 2), colW + borderSize * 2);

            columnWidths [col] = colW;
            totalW += colW;
            totalH = jmax (totalH, colH);

            childNum += numChildren;
        }

        return totalW;
    }

    int updateYPositions()
    {
        if (isScrolling)
        {
            if (childYOffset < 0)
                childYOffset = 0;

            const int actualH = getHeight() - borderSize * 2;

            if (childYOffset > totalH - (actualH - scrollZone * 2))
                childYOffset = totalH - (actualH - scrollZone * 2);
        }
        else
        {
            childYOffset = 0;
        }

        int x = 0;
        int childNum = 0;

        for (int col = 0; col < numColumns; ++col)
        {
            const int numChildren = jmin (getNumChildComponents() - childNum,
                                          (getNumChildComponents() + numColumns - 1) / numColumns);

            const int colW = columnWidths [col];

            int y = borderSize - childYOffset;

            if (isScrolling)
                y += scrollZone;

            for (int i = 0; i < numChildren; ++i)
            {
                Component* const c = getChildComponent (childNum + i);
                c->setBounds (x, y, colW, c->getHeight());
                y += c->getHeight();
            }

            x += colW;
            childNum += numChildren;
        }

        return x;
    }

    void setCurrentlyHighlightedChild (MenuItemComponent* const child)
    {
        if (currentChild->isValidComponent())
            currentChild->setHighlighted (false);

        currentChild = child;

        if (currentChild != 0)
        {
            currentChild->setHighlighted (true);
            timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter();
        }
    }

    bool showSubMenuFor (MenuItemComponent* const childComp)
    {
        jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent());
        deleteAndZero (activeSubMenu);

        if (childComp->isValidComponent() && childComp->itemInfo.hasActiveSubMenu())
        {
            activeSubMenu = PopupMenuWindow::create (*(childComp->itemInfo.subMenu),
                                                     dismissOnMouseUp,
                                                     this,
                                                     childComp->getScreenX(),
                                                     childComp->getScreenX() + childComp->getWidth(),
                                                     childComp->getScreenY(),
                                                     childComp->getScreenY() + childComp->getHeight(),
                                                     false, menuBarComponent);

            if (activeSubMenu != 0)
            {
                activeSubMenu->setVisible (true);
                activeSubMenu->grabKeyboardFocus();
                activeSubMenu->enterModalState();

                return true;
            }
        }

        return false;
    }

    void highlightItemUnderMouse (const int mx, const int my, const int x, const int y)
    {
        isOver = reallyContains (x, y, true);

        if (isOver)
            hasBeenOver = true;

        if (abs (lastMouseX - mx) > 2 || abs (lastMouseY - my) > 2)
        {
            lastMouseMoveTime = Time::getApproximateMillisecondCounter();

            if (disableMouseMoves && isOver)
                disableMouseMoves = false;
        }

        if (disableMouseMoves)
            return;

        bool isMovingTowardsMenu = false;

        jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent())

        if (isOver && (activeSubMenu != 0) && (mx != lastMouseX || my != lastMouseY))
        {
            // try to intelligently guess whether the user is moving the mouse towards a currently-open
            // submenu. To do this, look at whether the mouse stays inside a triangular region that
            // extends from the last mouse pos to the submenu's rectangle..

            float subX = (float) activeSubMenu->getScreenX();

            if (activeSubMenu->getX() > getX())
            {
                lastMouseX -= 2;  // to enlarge the triangle a bit, in case the mouse only moves a couple of pixels
            }
            else
            {
                lastMouseX += 2;
                subX += activeSubMenu->getWidth();
            }

            Path areaTowardsSubMenu;
            areaTowardsSubMenu.addTriangle ((float) lastMouseX,
                                            (float) lastMouseY,
                                            subX,
                                            (float) activeSubMenu->getScreenY(),
                                            subX,
                                            (float) (activeSubMenu->getScreenY() + activeSubMenu->getHeight()));

            isMovingTowardsMenu = areaTowardsSubMenu.contains ((float) mx, (float) my);
        }

        lastMouseX = mx;
        lastMouseY = my;

        if (! isMovingTowardsMenu)
        {
            Component* c = getComponentAt (x, y);
            if (c == this)
                c = 0;

            MenuItemComponent* mic = dynamic_cast <MenuItemComponent*> (c);

            if (mic != currentChild
                 && (isOver || (activeSubMenu == 0) || ! activeSubMenu->isVisible()))
            {
                if (isOver && (c != 0) && (activeSubMenu != 0))
                {
                    activeSubMenu->hide (0);
                    grabKeyboardFocus();
                }

                if (! isOver)
                    mic = 0;

                setCurrentlyHighlightedChild (mic);
            }
        }
    }

    void triggerCurrentlyHighlightedItem()
    {
        if (currentChild->isValidComponent()
             && currentChild->itemInfo.canBeTriggered())
        {
            dismissMenu (&currentChild->itemInfo);
        }
    }

    void selectNextItem (const int delta)
    {
        disableTimerUntilMouseMoves();
        MenuItemComponent* mic = 0;
        bool wasLastOne = (currentChild == 0);
        const int numItems = getNumChildComponents();

        for (int i = 0; i < numItems + 1; ++i)
        {
            int index = (delta > 0) ? i : (numItems - 1 - i);
            index = (index + numItems) % numItems;

            mic = dynamic_cast <MenuItemComponent*> (getChildComponent (index));

            if (mic != 0 && (mic->itemInfo.canBeTriggered() || mic->itemInfo.hasActiveSubMenu())
                 && wasLastOne)
                break;

            if (mic == currentChild)
                wasLastOne = true;
        }

        setCurrentlyHighlightedChild (mic);
    }

    void disableTimerUntilMouseMoves()
    {
        disableMouseMoves = true;

        if (owner != 0)
            owner->disableTimerUntilMouseMoves();
    }

    PopupMenuWindow (const PopupMenuWindow&);
    const PopupMenuWindow& operator= (const PopupMenuWindow&);
};


//==============================================================================
PopupMenu::PopupMenu()
    : items (8),
      lookAndFeel (0)
{
}

PopupMenu::PopupMenu (const PopupMenu& other)
    : items (8),
      lookAndFeel (other.lookAndFeel)
{
    items.ensureStorageAllocated (other.items.size());

    for (int i = 0; i < other.items.size(); ++i)
        items.add (new MenuItemInfo (*(const MenuItemInfo*) other.items.getUnchecked(i)));
}

const PopupMenu& PopupMenu::operator= (const PopupMenu& other)
{
    if (this != &other)
    {
        lookAndFeel = other.lookAndFeel;

        clear();
        items.ensureStorageAllocated (other.items.size());

        for (int i = 0; i < other.items.size(); ++i)
            items.add (new MenuItemInfo (*(const MenuItemInfo*) other.items.getUnchecked(i)));
    }

    return *this;
}

PopupMenu::~PopupMenu()
{
    clear();
}

void PopupMenu::clear()
{
    for (int i = items.size(); --i >= 0;)
    {
        MenuItemInfo* const mi = (MenuItemInfo*) items.getUnchecked(i);
        delete mi;
    }

    items.clear();
}

void PopupMenu::addItem (const int itemResultId,
                         const String& itemText,
                         const bool isActive,
                         const bool isTicked,
                         const Image* const iconToUse,
                         KeyPressMappingSet* const keyMappingSetToTrigger,
                         const int keyMappingCommandUID)
{
    jassert (itemResultId != 0);    // 0 is used as a return value to indicate that the user
                                    // didn't pick anything, so you shouldn't use it as the id
                                    // for an item..

    items.add (new MenuItemInfo (itemResultId,
                                 itemText,
                                 isActive,
                                 isTicked,
                                 iconToUse,
                                 Colours::black,
                                 false,
                                 0, 0,
                                 keyMappingSetToTrigger,
                                 keyMappingCommandUID));
}

void PopupMenu::addColouredItem (const int itemResultId,
                                 const String& itemText,
                                 const Colour& itemTextColour,
                                 const bool isActive,
                                 const bool isTicked,
                                 const Image* const iconToUse,
                                 KeyPressMappingSet* const keyMappingSetToTrigger,
                                 const int keyMappingCommandUID)
{
    jassert (itemResultId != 0);    // 0 is used as a return value to indicate that the user
                                    // didn't pick anything, so you shouldn't use it as the id
                                    // for an item..

    items.add (new MenuItemInfo (itemResultId,
                                 itemText,
                                 isActive,
                                 isTicked,
                                 iconToUse,
                                 itemTextColour,
                                 true,
                                 0, 0,
                                 keyMappingSetToTrigger,
                                 keyMappingCommandUID));
}

//==============================================================================
void PopupMenu::addCustomItem (const int itemResultId,
                               PopupMenuCustomComponent* const customComponent)
{
    jassert (itemResultId != 0);    // 0 is used as a return value to indicate that the user
                                    // didn't pick anything, so you shouldn't use it as the id
                                    // for an item..

    items.add (new MenuItemInfo (itemResultId,
                                 String::empty,
                                 true,
                                 false,
                                 0,
                                 Colours::black,
                                 false,
                                 customComponent,
                                 0, 0, 0));
}

class NormalComponentWrapper : public PopupMenuCustomComponent
{
public:
    NormalComponentWrapper (Component* const comp, const int w, const int h)
        : width (w), height (h)
    {
        addAndMakeVisible (comp);
    }

    ~NormalComponentWrapper() {}

    void getIdealSize (int& idealWidth, int& idealHeight)
    {
        idealWidth = width;
        idealHeight = height;
    }

    void resized()
    {
        if (getChildComponent(0) != 0)
            getChildComponent(0)->setBounds (0, 0, getWidth(), getHeight());
    }

private:
    const int width, height;

    NormalComponentWrapper (const NormalComponentWrapper&);
    const NormalComponentWrapper& operator= (const NormalComponentWrapper&);
};

void PopupMenu::addCustomItem (Component* customComponent, int idealWidth, int idealHeight)
{
    addCustomItem (0xa23593ff, new NormalComponentWrapper (customComponent, idealWidth, idealHeight));
}

//==============================================================================
void PopupMenu::addSubMenu (const String& subMenuName,
                            const PopupMenu& subMenu,
                            const bool isActive)
{
    items.add (new MenuItemInfo (0,
                                 subMenuName,
                                 isActive && (subMenu.getNumItems() > 0),
                                 false,
                                 0,
                                 Colours::black,
                                 false,
                                 0,
                                 &subMenu,
                                 0, 0));
}

void PopupMenu::addSeparator()
{
    items.add (new MenuItemInfo());
}

//==============================================================================
Component* PopupMenu::createMenuComponent (const int x, const int y, const int w, const int h,
                                           const int itemIdThatMustBeVisible,
                                           const bool alignToRectangle,
                                           Component* menuBarComponent)
{
    PopupMenuWindow* const pw
        = PopupMenuWindow::create (*this,
                                   ModifierKeys::getCurrentModifiers().isAnyMouseButtonDown(),
                                   0,
                                   x, x + w,
                                   y, y + h,
                                   alignToRectangle,
                                   menuBarComponent);

    if (pw != 0)
    {
        pw->setVisible (true);
        pw->toFront (true);
        pw->grabKeyboardFocus();
        pw->ensureItemIsVisible (itemIdThatMustBeVisible);
    }

    return pw;
}

int PopupMenu::showMenu (const int x, const int y, const int w, const int h,
                         const int itemIdThatMustBeVisible,
                         const bool alignToRectangle)
{
    Component* const prevFocused = Component::getCurrentlyFocusedComponent();

    ComponentDeletionWatcher* deletionChecker1 = 0;
    if (prevFocused != 0)
        deletionChecker1 = new ComponentDeletionWatcher (prevFocused);

    Component* const prevTopLevel = (prevFocused != 0) ? prevFocused->getTopLevelComponent() : 0;

    ComponentDeletionWatcher* deletionChecker2 = 0;
    if (prevTopLevel != 0)
        deletionChecker2 = new ComponentDeletionWatcher (prevTopLevel);

    wasHiddenBecauseOfAppChange = false;

    int result = 0;
    Component* const popupComp = createMenuComponent (x, y, w, h,
                                                      itemIdThatMustBeVisible,
                                                      alignToRectangle, 0);

    if (popupComp != 0)
    {
        result = popupComp->runModalLoop();
        delete popupComp;

        if (! wasHiddenBecauseOfAppChange)
        {
            if (deletionChecker2 != 0 && ! deletionChecker2->hasBeenDeleted())
                prevTopLevel->toFront (true);

            if (deletionChecker1 != 0 && ! deletionChecker1->hasBeenDeleted())
                prevFocused->grabKeyboardFocus();
        }
    }

    delete deletionChecker1;
    delete deletionChecker2;

    return result;
}

int PopupMenu::show (const int itemIdThatMustBeVisible)
{
    return showAt (Component::getMouseX(),
                   Component::getMouseY(),
                   itemIdThatMustBeVisible);
}

int PopupMenu::showAt (const int screenX,
                       const int screenY,
                       const int itemIdThatMustBeVisible)
{
    return showMenu (screenX, screenY, 1, 1, itemIdThatMustBeVisible, false);
}

int PopupMenu::showAt (Component* componentToAttachTo,
                       const int itemIdThatMustBeVisible)
{
    if (componentToAttachTo != 0)
        return showMenu (componentToAttachTo->getScreenX(),
                         componentToAttachTo->getScreenY(),
                         componentToAttachTo->getWidth(),
                         componentToAttachTo->getHeight(),
                         itemIdThatMustBeVisible, true);
    else
        return show();
}

//==============================================================================
int PopupMenu::getNumItems() const
{
    int num = 0;

    for (int i = items.size(); --i >= 0;)
        if (! ((MenuItemInfo*) items.getUnchecked(i))->isSeparator)
            ++num;

    return num;
}

void PopupMenu::setLookAndFeel (LookAndFeel* const newLookAndFeel)
{
    lookAndFeel = newLookAndFeel;
}

//==============================================================================
PopupMenuCustomComponent::PopupMenuCustomComponent()
    : refCount_(0)
{
}

PopupMenuCustomComponent::~PopupMenuCustomComponent()
{
    jassert (refCount_ == 0); // should be deleted only by the menu component, as they keep a ref-count.
}

void PopupMenuCustomComponent::triggerMenuItem()
{
    MenuItemComponent* const mic = dynamic_cast<MenuItemComponent*> (getParentComponent());

    if (mic != 0)
    {
        PopupMenuWindow* const pmw = dynamic_cast<PopupMenuWindow*> (mic->getParentComponent());

        if (pmw != 0)
        {
            pmw->dismissMenu (&mic->itemInfo);
        }
        else
        {
            // something must have gone wrong with the component hierarchy if this happens..
            jassertfalse
        }
    }
    else
    {
        // why isn't this component inside a menu? Not much point triggering the item if
        // there's no menu.
        jassertfalse
    }
}

END_JUCE_NAMESPACE
