A fancy popup circular menu


#1

A menu like this one:

The advantages:
[list]
[] Every command is equidistant from the clicked point, so it’s faster to select the command[/]
[] Humans have very good spatial & color memory, so reselecting an item is easier because it stays at the same place, with the same color[/]
[] Fancy animation improve overall user-friendly smoothness of the application [/]
[] Vector drawed (so it can be any size)[/]
[] More contextual (meaning no “unselected item” choice), appear when multiple action are possible[/]
[] Almost compatible with juce’s PopupMenu component[/][/list]

The cons:
[list]
[] Except for Maya users, the circular menus are not usual (but getting used to them is a matter of minutes)[/]
[] For the moment, no sub menu (this would requires even more math to render nicely, I never needed them before anyway)[/]
[] No disabled menu item (but is it useful anyway ?)[/]
[] No ticked/unticked state (could be added easily)[/][/list]

Let me know what you think about them

Edit: Forgot usage:

    CircularMenu menu;
    Drawable * editCopy = ..., *editCut = ..., *editPaste = ...;
    menu.addItem(1, editCopy, "Copy box");
    menu.addItem(2, editCut, "Cut box");
    menu.addItem(3, editPaste, "Paste box");
    menu.show(96 /*radius*/);

#2

CircularMenu.hpp

#ifndef hpp_CPP_CircularMenu_CPP_hpp
#define hpp_CPP_CircularMenu_CPP_hpp

// We need juce declaration
#include "juce.h"

namespace juce
{
    /** The circular menu is a popup menu with cool animation effect that tries to enhance ergonomy.
        All menu items are equidistant from the current mouse cursor position 
        so reaching any item is shorted than traditional menus.
        Menu items are big (depending on the circular menu radius), so targetting them is easy too.
        Menu items also always keep a similar color so action selection becomes a "reflex" 
        
        On the bad side, it's different from what user are used too, so it's up to you to choose what you prefer */
    class CircularMenu
    {
        // Type definition and enumeration
    private:
        /** This is a menu petal used to display a menu item */
        class MenuPetal  : public Component
        {
            // Members
        private:
            /** The petal path used for drawing */
            Path        petalPath;
            /** The menu icon (this is required, as this menu is graphic mainly) */
            Drawable &  icon;
            /** The menu ID */
            const int   itemID;
            /** The base menu color */
            Colour      baseColour;
            /** Do we have a sub menu ? */
            bool        withSubMenu;
            /** This petal surface angle in radian */
            float       surfaceAngle;
            /** This petal angle from X/Y axis */
            float       currentAngle;
            /** This petal pivot point */
            Point       pivot;    
            /** The current circle radius */
            float       petalRadius;
            /** This is the delta for the petal path's bounding box top left corner (this is used to speed up bounds checking) */
            Point       topLeftCorner;
            /** The image where the drawable was cached */
            Image *     iconImage;
            /** The icon top left corner (this is used to speed up painting too) */
            Point       iconTopLeftCorner;
            /** The component general offset */
            int         generalOffsetX, generalOffsetY;
            /** The item selection state */
            bool        isPreSelected;
           

            // Component interface
        public:
            /** Paint this petal */
            void paint (Graphics& g);
            /** Check if mouse is inside the petal */
            bool hitTest(int x, int y);
            /** Called when mouse enter this petal */
            void mouseEnter (const MouseEvent& e);
            /** Called when mouse leaves this petal */
            void mouseExit (const MouseEvent& e);
            /** Called when mouse button hasd been clicked on this petal */
            void mouseUp (const MouseEvent& e);


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

            // Our interface
        public:
            /** Rotate around the given pivot of the given angle 
                @param x    The pivot's horizontal position relative to this component 
                @param y    The pivot's vertical position relative to this component 
                @param angle    The angle in radian */
            void rotateAround(const float x, const float y, const float angle);
            /** Set the circle radius */
            void setCircleRadius(const float radius);
            /** Resize to fit given circle radius */
            void resizeToFit(const float radius, const float finalAngle);
            /** Set the general offset */
            void setGeneralOffset(const int x, const int y);
            /** Check if this item is selected */
            bool isSelected();
            /** Select this item */
            void makeSelected(const bool shouldBeSelected = true);

            // Helpers
        private:
            /** Get the icon position and size */
            const Rectangle getIconBounds(const float angle = -1) const;

            // Construction and destruction
        public:
            /** Construct a menu petal to display with the given parameters
                @param nbPetal          The number of petal in this menu 
                @param icon             A reference on a drawable used to display the item's icon
                @param itemID           The item ID  
                @param baseColour       Define the menu base colour 
                @param hasSubMenu       Set to true if clicking this menu will display a submenu */
            MenuPetal (const int nbPetal, Drawable & icon, const int itemID, const Colour & baseColour, const bool hasSubMenu = false);
            /** The destructor */
            ~MenuPetal();

        private:
            /** Prevent copy constructor being generated */
            MenuPetal (const MenuPetal&);
            /** Prevent operator= being generated too */
            const MenuPetal& operator= (const MenuPetal&);
        };

        // Members
    private:
        /** The icons array */
        OwnedArray<Drawable>    icons;
        /** The item id */
        Array<int>              itemsId;
        /** The item title array */
        StringArray             itemTexts;

        // Interface
    public:
        /** Appends a new item for this menu to show.

            @param itemResultId     the number that will be returned from the show() method
                                    if the user picks this item. The value should never be
                                    zero, because that's used to indicate that the user didn't
                                    select anything.
            @param itemText         the text to show.
            @param isTicked         if true, the item will be shown with a tick next to it
            @param iconToUse        if this is non-zero, it should be an drawable that will be
                                    displayed in the menu petal. This method will take ownership of the drawable.

            @return true on success, false if the insertion couldn't be performed
            @see show
        */
        bool addItem (const int itemResultId,
                      Drawable* iconToUse,
                      const String& itemText
//                      const bool isTicked = false,
                      ) throw();

        /** Returns the number of items that the menu currently contains. */
        int getNumItems() const throw() { return itemsId.size(); }

        /** Displays the menu and waits for the user to pick something.

            This will display the menu modally, and return the ID of the item that the
            user picks. If they click somewhere off the menu to get rid of it without
            choosing anything, this will return 0.

            The current location of the mouse will be used as the position to show the
            menu - to explicitly set the menu's position, use showAt() instead. Depending
            on where this point is on the screen, the menu will appear above, below or
            to the side of the point.

            @param minimumRadius            a minimum radius for the menu, in pixels. It may be wider
                                            than this if some items are too long to fit.
            @see showAt
        */
        int show (const int minimumRadius);
        /** Displays the menu at a specific location.

            This is the same as show(), but uses a specific location (in global screen
            co-ordinates) rather than the current mouse position.

            @see show()
        */
        int showAt (const int screenX,
                    const int screenY,
                    const int minimumRadius);

        // Helpers
    private:
        /** Create the popup menu window */
        Component* createMenuComponent (const int x, const int y, const int w, const int h, ApplicationCommandManager** managerOfChosenCommand, Component* const componentAttachedTo) throw();
        friend struct CircularMenuWindow;
    };
}


#endif

#3

CircularMenu.cpp

// We need our declaration
#include "CircularMenu.hpp"

namespace juce
{
    static const float Pi = 3.1415956535f;
    static const float iconAlpha = 0.6f;
    static const int dismissCommandId = 0x6287345f;
    static VoidArray activeMenuWindows;


    struct CircularMenuWindow  : public Component,
                                private Timer
    {
        enum 
        { 
            timerInterval = 10,
            maxNbSteps = 6,
            FadeIn          = 0,
            Unfold          = 1,
            UnfoldAndFadeIn = 2,
            ShowLabels      = 3,
            Static          = 4,
            FoldAndFadeOut  = 5,
        };

        uint32 menuCreationTime, lastFocused, lastScroll, lastMouseMoveTime, timeEnteredCurrentChildComp;
        CircularMenuWindow*         activeSubMenu;
        ComponentDeletionWatcher*   attachedCompWatcher;
        Component* const            componentAttachedTo;
        ApplicationCommandManager** managerOfChosenCommand;
        int lastMouseX, lastMouseY;
        ReduceOpacityEffect makeTransparent;
        GlowEffect    dropShadow;
        int         currentAnimationPhase;
        int         currentAnimationSteps;
        int         selectedItemID;
        Point       menuCenter;
        float       radius; 


        Array<Label*>           labels;

        OwnedArray<Drawable> &  icons;
        Array<int> &            itemsID;
        StringArray &           itemsText;

        CircularMenuWindow(const int screenX, const int screenY, const int width, const int height, 
                            OwnedArray<Drawable> & _icons, Array<int> & _itemsID, StringArray & _itemsText, ApplicationCommandManager** _managerOfChosenCommand, Component* const _componentAttachedTo)
            : icons(_icons),
              itemsID(_itemsID),
              itemsText(_itemsText),
              activeSubMenu(0),
              attachedCompWatcher(0),
              makeTransparent(0),
              selectedItemID(0),
              radius((float)width / 2),
              managerOfChosenCommand(_managerOfChosenCommand),
              componentAttachedTo(_componentAttachedTo),
              currentAnimationPhase(4),
              currentAnimationSteps(0)
                
              
        {
            menuCreationTime = lastFocused = lastScroll = Time::getMillisecondCounter();
            setWantsKeyboardFocus (true);

            attachedCompWatcher = componentAttachedTo != 0 ? new ComponentDeletionWatcher (componentAttachedTo) : 0;

            setOpaque (false);
            setAlwaysOnTop (true);

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

            activeMenuWindows.add (this);

            Rectangle rect;
            // At first, the petal are all transparent
            // Then add and dispatch the petals
            for (int i = 0; i < itemsID.size(); i++)
            {
                // Create the petal now
                Colour  baseColour = Colours::mediumpurple;
                CircularMenu::MenuPetal * petal = new CircularMenu::MenuPetal(itemsID.size(), *icons[i], itemsID[i], baseColour.withRotatedHue((float)i / (float)itemsID.size()), false);
                petal->resizeToFit((float)width/2, (float)i * 2 * 3.1415926535f / itemsID.size());
                if (i == 0) rect = petal->getBounds();
                else rect = rect.getUnion(petal->getBounds());
                     
                // Then rotate the petal to the final position (will be removed afterward)
                // petal->rotateAround((float)width / 2, (float)height/2, (float)i * 2 * 3.1415926535f / itemsID.size());
                // Then add this object
                addAndMakeVisible(petal);
                // Set the component effect
                petal->setComponentEffect(&makeTransparent);
            }


            
            addToDesktop (ComponentPeer::windowIsTemporary);
            setBounds(screenX /*+ rect.getX()*/, screenY /*+ rect.getY()*/, width, height);
            menuCenter.setXY((float)width / 2, (float)height / 2);

            Rectangle currentBounds = getBounds();
            Point topLeft((float)currentBounds.getX(), (float)currentBounds.getY());
            for (i = 0; i < itemsID.size(); i++)
            {
                // Create the label at first
                Label * label = new Label(String("MenuLabel") << i, itemsText[i]);
                if (i == 0)
                {
                    Font labelFont = label->getFont();
                    labelFont.setBold(true);
                    label->setFont(labelFont);
                }

                // Then compute the required label size
                float labelHeight = label->getFont().getHeight() + 2.0f;
                float labelWidth = label->getFont().getStringWidthFloat(label->getText()) + 5.0f;

                // Compute the center point now 
                float x = radius + labelWidth / 2 + 5.0f;
                float y = 0;

                // Rotate it for the given petal
                const AffineTransform & rotationUsed = AffineTransform::rotation((float)i * 2 * Pi / itemsID.size());
                rotationUsed.transformPoint(x, y);

                // Then translate to the current origin
                x += menuCenter.getX();
                y += menuCenter.getY();

                // Then add it to the array and child component
                labels.add(label);
                // Define the current label bound
                label->setBounds(roundFloatToInt(x - labelWidth/2), roundFloatToInt(y - labelHeight/2), roundFloatToInt(labelWidth), roundFloatToInt(labelHeight));

                currentBounds = currentBounds.getUnion(label->getBounds().translated(roundFloatToInt(topLeft.getX()), roundFloatToInt(topLeft.getY())));
            }
            // Then, we need to enlarge our current bounds to include the labels
            menuCenter.setXY(menuCenter.getX() + (topLeft.getX() - currentBounds.getX()), menuCenter.getY() + (topLeft.getY() - currentBounds.getY()));
            setBounds(currentBounds);

            // Now move every petal to their respective positions
            for (i = 0; i < itemsID.size(); i++)
            {
                Point componentCenter((float)(getChildComponent(i)->getX() + getChildComponent(i)->getWidth() / 2), (float)(getChildComponent(i)->getY() + getChildComponent(i)->getHeight() / 2));
                getChildComponent(i)->setCentrePosition(roundFloatToInt(componentCenter.getX() + topLeft.getX() - currentBounds.getX()), roundFloatToInt(componentCenter.getY() + topLeft.getY() - currentBounds.getY()));
                CircularMenu::MenuPetal * menu = dynamic_cast<CircularMenu::MenuPetal*>(getChildComponent(i));
                menu->setGeneralOffset(roundFloatToInt(topLeft.getX() - currentBounds.getX()), roundFloatToInt(topLeft.getY() - currentBounds.getY()));
            }

            // And adjust the label final bounds too
            for (i = 0; i < labels.size(); i++)
            {
                Label * label = labels[i];
                Point componentCenter((float)(label->getX() + label->getWidth() / 2), (float)(label->getY() + label->getHeight() / 2));
                label->setCentrePosition(roundFloatToInt(componentCenter.getX() + topLeft.getX() - currentBounds.getX()), roundFloatToInt(componentCenter.getY() + topLeft.getY() - currentBounds.getY()));
                // Stupid hack to avoid popping artefact
                addChildComponent(label);
//                label->setComponentEffect(&dropShadow);
                label->setVisible(false);
                label->addMouseListener(this, false);
            }

            //dropShadow.setShadowProperties(2, 0.8f, 1, 1);
            dropShadow.setGlowProperties(6.0f, Colours::white);
            setVisible(true);
            // Unfold the petal now
            unfoldPetals();
        }

        ~CircularMenuWindow()
        {
            activeMenuWindows.removeValue (this);

            Desktop::getInstance().removeGlobalMouseListener (this);
            jassert (activeSubMenu == 0 || activeSubMenu->isValidComponent());
            delete activeSubMenu;
            
            deleteAllChildren(); delete attachedCompWatcher;
        }
        void paint(Graphics& g)
        {
//            g.drawRect(menuCenter.getX() - 2, menuCenter.getY() - 2, 4, 4);
        }

        void unfoldPetals()
        {
            currentAnimationPhase = UnfoldAndFadeIn;
            currentAnimationSteps = 0;
            startTimer(timerInterval);
        }

        void foldPetalsAndExit(const int selectedId)
        {
            stopTimer();
            currentAnimationSteps = 0;
            currentAnimationPhase = FoldAndFadeOut;
            selectedItemID = selectedId;
            startTimer(timerInterval);
        }

        void mouseEnter (const MouseEvent& e)
        {
            // Find which label this event is relative too
            Label * label = dynamic_cast<Label*>(e.eventComponent);
            if (label)
            {
                // Ok, now find the label index
                for (int i = 0; i < itemsText.size(); i++)
                {
                    ((CircularMenu::MenuPetal*)getChildComponent(i))->makeSelected(itemsText[i] == label->getText());
                }
            }
        }
        /** Called when mouse leaves this petal */
        void mouseExit (const MouseEvent& e)
        {
            // Find which label this event is relative too
            Label * label = dynamic_cast<Label*>(e.eventComponent);
            if (label)
            {
                // Ok, now find the label index
                for (int i = 0; i < itemsText.size(); i++)
                {
                    if (itemsText[i] == label->getText())
                        ((CircularMenu::MenuPetal*)getChildComponent(i))->makeSelected(false);
                }
            }
        }
        /** Called when mouse button hasd been clicked on this petal */
        void mouseUp (const MouseEvent& e)
        {
            // Find which label this event is relative too
            Label * label = dynamic_cast<Label*>(e.eventComponent);
            if (label)
            {
                // Ok, now find the label index
                for (int i = 0; i < itemsText.size(); i++)
                {
                    if (itemsText[i] == label->getText())
                        foldPetalsAndExit(itemsID[i]);
                }
            }
        }
        

        bool isOverAnyMenu()
        {
            return false;
        }

        void inputAttemptWhenModal()
        {
            timerCallback();

            if (! isOverAnyMenu())
            {
                if (componentAttachedTo != 0 && ! attachedCompWatcher->hasBeenDeleted())
                {
                    // we want to dismiss the menu, but if we do it synchronously, then
                    // the mouse-click will be allowed to pass through. That's good, except
                    // when the user clicks on the button that orginally popped the menu up,
                    // as they'll expect the menu to go away, and in fact it'll just
                    // come back. So only dismiss synchronously if they're not on the original
                    // comp that we're attached to.
                    int mx, my;
                    componentAttachedTo->getMouseXYRelative (mx, my);

                    if (componentAttachedTo->reallyContains (mx, my, true))
                    {
                        postCommandMessage (dismissCommandId); // dismiss asynchrounously
                        return;
                    }
                }
                this->foldPetalsAndExit(0);
            }
        }

        void handleCommandMessage (int commandId)
        {
            Component::handleCommandMessage (commandId);

            if (commandId == dismissCommandId)
                foldPetalsAndExit (0);
        }


        void triggerCurrentlyHighlightedItem()
        {
            // Find the currently selected petal
            int i = 0, currentSelection = -1;
            for (i = 0; i < itemsID.size(); i++)
            {
                CircularMenu::MenuPetal * petal = dynamic_cast<CircularMenu::MenuPetal*>(getChildComponent(i));
                if (petal && petal->isSelected()) 
                {
                    currentSelection = i;
                    break;
                }
            }
            if (currentSelection < 0 || currentSelection >= itemsID.size()) currentSelection = 0;  
                
            foldPetalsAndExit(itemsID[currentSelection]);
        }

        void selectNextItem (const int delta)
        {
            // Find the currently selected petal
            int i = 0, currentSelection = -1;
            for (i = 0; i < itemsID.size(); i++)
            {
                CircularMenu::MenuPetal * petal = dynamic_cast<CircularMenu::MenuPetal*>(getChildComponent(i));
                if (petal && petal->isSelected()) 
                {
                    currentSelection = i;
                    break;
                }
            }

            // Then adjust the selection
            currentSelection += delta;
            if (currentSelection < 0) currentSelection = itemsID.size() - 1;
            if (currentSelection >= itemsID.size()) currentSelection = 0;

            // And apply
            for (i = 0; i < itemsID.size(); i++)
            {
                CircularMenu::MenuPetal * petal = dynamic_cast<CircularMenu::MenuPetal*>(getChildComponent(i));
                if (petal) petal->makeSelected(i == currentSelection); 
            }
        }

        bool keyPressed (const KeyPress& key)
        {
            if (key.isKeyCode (KeyPress::downKey))
            {
                selectNextItem (1);
            }
            else if (key.isKeyCode (KeyPress::upKey))
            {
                selectNextItem (-1);
            }
            else if (key.isKeyCode (KeyPress::returnKey))
            {
                triggerCurrentlyHighlightedItem();
            }
            else if (key.isKeyCode (KeyPress::escapeKey))
            {
                foldPetalsAndExit(0);
            }
            else
            {
                return false;
            }

            return true;
        }

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

            if (attachedCompWatcher != 0 && attachedCompWatcher->hasBeenDeleted())
            {
                //dismissMenu (0);
                return;
            }

            switch(currentAnimationPhase)
            {
            case FadeIn:  // We make the petal appear 
                {
                    // The number of steps in this phase is 10
                    makeTransparent.setOpacity((float)(++currentAnimationSteps) / (float)maxNbSteps);
                    for (int i = 0; i < itemsID.size(); i++)
                    {
                        this->getChildComponent(i)->repaint();
                    }
                    if (currentAnimationSteps == maxNbSteps) 
                    {
                        currentAnimationSteps = 0;
                        currentAnimationPhase = Unfold;
                    }
                    break;
                }
            case Unfold:  // We rotate petals
                {
                    currentAnimationSteps++;
                    for (int i = 0; i < itemsID.size(); i++)
                    {
                        CircularMenu::MenuPetal * petal = dynamic_cast<CircularMenu::MenuPetal*>(this->getChildComponent(i));
                        if (petal)
                            petal->rotateAround(menuCenter.getX(), menuCenter.getY(), (float)i * 2 * Pi * (float)(currentAnimationSteps / (float)maxNbSteps) / itemsID.size());
                    }
                    if (currentAnimationSteps == maxNbSteps) 
                    {
                        currentAnimationSteps = 0;
                        currentAnimationPhase = ShowLabels;
                    }
                }
                break;
            case UnfoldAndFadeIn:  // We both make the petal appear and rotate
                {
                    // The number of steps in this phase is 10
                    makeTransparent.setOpacity((float)(++currentAnimationSteps) / (float)maxNbSteps);
                    for (int i = 0; i < itemsID.size(); i++)
                    {
                        CircularMenu::MenuPetal * petal = dynamic_cast<CircularMenu::MenuPetal*>(this->getChildComponent(i));
                        if (petal)
                            petal->rotateAround(menuCenter.getX(), menuCenter.getY(), (float)i * 2 * Pi * (float)(currentAnimationSteps  / (float)maxNbSteps) / itemsID.size());
                    }
                    if (currentAnimationSteps == maxNbSteps) 
                    {
                        currentAnimationSteps = 0;
                        currentAnimationPhase = ShowLabels;
                    }
                    break;
                }
            case ShowLabels:
                {
                    // Try to work out in 3 phases
//                    static Array<Component*> desktopComponents;
                    for (int i = 0; i < labels.size(); i++)
                    {
/*
                        labels[i]->setColour(Label::backgroundColourId, Colours::white);
                        labels[i]->setColour(Label::outlineColourId, Colours::transparentWhite);
                        labels[i]->setColour(Label::textColourId, Colours::black);
                        */
                        labels[i]->setComponentEffect(&dropShadow);

                        labels[i]->setVisible(true);

                    }

                    currentAnimationSteps = 0;
                    currentAnimationPhase = Static;
                }
                break;

            case Static: // This is static step
                break;

            case FoldAndFadeOut: // We fold and make petal disappear
                {
                    // The number of steps in this phase is 10
                    makeTransparent.setOpacity(1.0f - (float)(++currentAnimationSteps) / (float)maxNbSteps);
                    for (int i = 0; i < itemsID.size(); i++)
                    {
                        CircularMenu::MenuPetal * petal = dynamic_cast<CircularMenu::MenuPetal*>(this->getChildComponent(i));
                        if (petal)
                            petal->rotateAround(menuCenter.getX(), menuCenter.getY(), (float)i * 2 * Pi * (1.0f - (float)(currentAnimationSteps  / (float)maxNbSteps)) / itemsID.size());
                    }
                    if (currentAnimationSteps == maxNbSteps) 
                    {
                        currentAnimationSteps = 0;
                        currentAnimationPhase = Static;
                        exitModalState(selectedItemID);
                    }
                    break;
                }
            }

            int mx, my;
            Desktop::getMousePosition (mx, my);

            int x = mx, y = my;
            globalPositionToRelative (x, y);

            const uint32 now = Time::getMillisecondCounter();
/*
            if (now > timeEnteredCurrentChildComp + 100
                 && reallyContains (x, y, true)
                 && currentChild->isValidComponent()
                 && (! disableMouseMoves)
                 && ! (activeSubMenu != 0 && activeSubMenu->isVisible()))
            {
                showSubMenuFor (currentChild);
            }
*/
        }

 
    };




    //==================================== Petals
    CircularMenu::MenuPetal::MenuPetal(const int nbPetal, Drawable & _icon, const int _itemID, const Colour & _baseColour, const bool hasSubMenu)
        : icon(_icon), baseColour(_baseColour.withAlpha(0.4f)), withSubMenu(hasSubMenu), 
          surfaceAngle(2 * Pi / (float)nbPetal), itemID(_itemID), isPreSelected(false),
          currentAngle(0), petalRadius(0), iconImage(0), generalOffsetX(0), generalOffsetY(0)
    {
    }

    CircularMenu::MenuPetal::~MenuPetal()
    {
        delete iconImage;
        deleteAllChildren();
    }

    void CircularMenu::MenuPetal::resizeToFit(const float radius, const float finalAngle)
    {
        setCircleRadius(radius);

        // This part need work too
        // Then move the label now too
//        label->setBounds(roundFloatToInt(radius * 2), roundFloatToInt(radius - label->getFont().getHeight() * 0.5f), label->getFont().getStringWidth(label->getText()) + 5, roundFloatToInt(label->getFont().getHeight()));
//        Rectangle labelBounds = label->getBounds();
        float x, y, w, h;
        petalPath.getBounds(x, y, w, h);
        Rectangle petalBounds(roundFloatToInt(x), roundFloatToInt(y), roundFloatToInt(w), roundFloatToInt(h));
        topLeftCorner.setXY(-x, -y);

        setBounds(petalBounds.translated(generalOffsetX, generalOffsetY));

        // Then cache the icon image
        delete iconImage;
        // Get the icon bounds 
        const Rectangle & iconBounds = getIconBounds(finalAngle);
        iconImage = new Image(Image::ARGB, iconBounds.getWidth(), iconBounds.getHeight(), false);
        iconImage->clear(0, 0, iconBounds.getWidth(), iconBounds.getHeight(), Colours::transparentWhite);
        Graphics g(*iconImage);
        icon.drawWithin(g, 0, 0, iconBounds.getWidth(), iconBounds.getHeight(), RectanglePlacement(RectanglePlacement::fillDestination | RectanglePlacement::centred | RectanglePlacement::stretchToFit));
        iconTopLeftCorner.setXY((float)iconBounds.getX(), (float)iconBounds.getY());
        iconImage->multiplyAllAlphas(iconAlpha);
    }

    void CircularMenu::MenuPetal::setCircleRadius(const float radius)
    {
        // We need to create the path for this petal now
        petalPath.clear();
        Path workingPath;

        workingPath.addPieSegment(0, 0, 2*radius, 2*radius, -surfaceAngle / 2 + Pi*0.5f, surfaceAngle/2 + Pi*0.5f, 0.1f);
//        workingPath.addRectangle(0, 0, radius, radius);
        petalPath = workingPath.createPathWithRoundedCorners(radius * 0.2f);
//        petalPath.applyTransform(AffineTransform::translation(-radius, -radius));


        // Ok, done
        petalRadius = radius;
    }

    inline float squareDistanceBetween(const Point & a, const Point & b)
    {
        return (a.getX() - b.getX())* (a.getX() - b.getX()) + (a.getY() - b.getY()) * (a.getY() - b.getY());
    }

    const Rectangle CircularMenu::MenuPetal::getIconBounds(const float angle) const
    {
        // What is the biggest dimension for this drawable ?
        float ix, iy, iw, ih;
        icon.getBounds(ix, iy, iw, ih);

        // Compute the maximum height, once we know the aspect ratio for the drawable
        double ar = iw / ih;
        double lambda = 0.5f * cos(surfaceAngle/2) + ar;
        double maximumHeight = (double)petalRadius / lambda * cos (atan(0.5f / lambda));
        
        // Once we have the maximum height, we can deduce the maximum width
        double maximumWidth = maximumHeight * ar;
        double minHorizontalPositionForIcon = cos(surfaceAngle / 2) * maximumHeight / 2;
        double minVerticalPositionForIcon = (float)petalRadius - maximumHeight/2;

        // Need to rotate this center point too
        float x = (float)(minHorizontalPositionForIcon + maximumWidth / 2);
        float y = 0;
        AffineTransform::rotation(angle == -1 ? currentAngle : angle).transformPoint(x, y);

        // Because we always draw the icons straight up, the bounding rectangle 
        // could overflow the circle limit
        // So check this case, and deduce the size factor
        double sizeFactor = 0.9f;
        // Find the icon bottom left corner position
        Point blc(x - (float)maximumWidth / 2, y + (float)maximumHeight / 2);
        Point tlc(x - (float)maximumWidth / 2, y - (float)maximumHeight / 2);
        Point trc(x + (float)maximumWidth / 2, y - (float)maximumHeight / 2);
        Point brc(x + (float)maximumWidth / 2, y + (float)maximumHeight / 2);
        // Check if any point is outside the circle
        float squareRadius = petalRadius * petalRadius;
        Point center(0, 0);
        float distance = squareDistanceBetween(center, blc);
        float maxDistance = distance;
        distance = squareDistanceBetween(center, brc);
        maxDistance = jmax(maxDistance, distance);
        distance = squareDistanceBetween(center, trc);
        maxDistance = jmax(maxDistance, distance);
        distance = squareDistanceBetween(center, tlc);
        maxDistance = jmax(maxDistance, distance);

        if (maxDistance * sizeFactor * sizeFactor > squareRadius)
        {
            // Need to resize the value to match the given distance
            sizeFactor *= petalRadius / sqrt(maxDistance);
        }

        // And finally compute the icon bounds
        return Rectangle(roundDoubleToInt(x + topLeftCorner.getX() - maximumWidth / 2 + maximumWidth * (1.0f - sizeFactor) * 0.5f + petalRadius), roundDoubleToInt(y + topLeftCorner.getY() + petalRadius - maximumHeight / 2 + maximumHeight * (1.0f - sizeFactor) * 0.5f), roundDoubleToInt(maximumWidth * sizeFactor), roundDoubleToInt(maximumHeight * sizeFactor));
    }

    bool CircularMenu::MenuPetal::hitTest(int x, int y)
    {
        // Check if we are inside the petal area
        return petalPath.contains(x - topLeftCorner.getX(), y - topLeftCorner.getY());
    }


    void CircularMenu::MenuPetal::paint(Graphics& g)
    {   
        //g.fillAll(Colours::grey);
        float x, y, w, h;
        petalPath.getBounds(x, y, w, h);
        Rectangle petalBounds(roundFloatToInt(0), roundFloatToInt(0), roundFloatToInt(w), roundFloatToInt(h));

        x = petalRadius * 0.1f; y = 0;
        AffineTransform rotationUsed = AffineTransform::rotation(currentAngle);
        rotationUsed.transformPoint(x, y);
        Point gradientPoint1(x + topLeftCorner.getX() + petalRadius, y + topLeftCorner.getY() + petalRadius);
        x = petalRadius * 0.95f; y = 0;
        rotationUsed.transformPoint(x, y);
        Point gradientPoint2(x + topLeftCorner.getX() + petalRadius, y + topLeftCorner.getY() + petalRadius);

        // The hardest part, I guess 
        Colour workingColour = baseColour;
        GradientBrush gradient_2 (workingColour.withMultipliedAlpha(0.16f),
                              gradientPoint1.getX(), gradientPoint1.getY(),
                              workingColour.withMultipliedAlpha(1.0f),
                              gradientPoint2.getX(), gradientPoint2.getY(),
                              false);
        g.setBrush (&gradient_2);
        // Fill the petal path
        Path workingPath(petalPath);
        workingPath.applyTransform(AffineTransform::translation(topLeftCorner.getX(), topLeftCorner.getY()));
        g.fillPath(workingPath);
        // And the outline too
//        g.setColour (Colour (0x661b1b1b));
        workingColour = workingColour.darker();
        GradientBrush gradient_3 (workingColour.withMultipliedAlpha(0.04f),
                              gradientPoint1.getX(), gradientPoint1.getY(),
                              workingColour.withMultipliedAlpha(1.0f),
                              gradientPoint2.getX(), gradientPoint2.getY(),
                              false);
        g.setBrush (&gradient_3);
        g.strokePath (workingPath, PathStrokeType (1.0000f));

        // Then draw the drawable too 
        // This is a little bit more complex here
        
        
        // We reduce the icon a little bit too to give free space on it (so it's more pleasant)
        g.drawImageAt(iconImage, roundFloatToInt(iconTopLeftCorner.getX()), roundFloatToInt(iconTopLeftCorner.getY()));
    }
    
    void CircularMenu::MenuPetal::rotateAround(const float _x, const float _y, const float angle)
    {
        // Undo the previous rotation
        petalPath.applyTransform(AffineTransform::rotation(-currentAngle, pivot.getX(), pivot.getY()));
        // Then rotate
        pivot.setXY(_x - generalOffsetX, _y - generalOffsetY);
        currentAngle = angle;
        petalPath.applyTransform(AffineTransform::rotation(currentAngle, pivot.getX(), pivot.getY()));

        {
            float x, y, w, h;
            petalPath.getBounds(x, y, w, h);
            Rectangle petalBounds(roundFloatToInt(x), roundFloatToInt(y), roundFloatToInt(w), roundFloatToInt(h));
            topLeftCorner.setXY(-x, -y);

            setBounds(petalBounds.translated(generalOffsetX, generalOffsetY));
            const Rectangle & iconBounds = getIconBounds(angle);
            iconTopLeftCorner.setXY((float)iconBounds.getX(), (float)iconBounds.getY());
        }
    }

    void CircularMenu::MenuPetal::mouseUp(const MouseEvent& e)
    {
        // Dismiss the parent modal loop
        CircularMenuWindow * window = dynamic_cast<CircularMenuWindow*>(getParentComponent());
        if (window)
            window->foldPetalsAndExit(itemID);
        else getParentComponent()->exitModalState(itemID);
    }

    void CircularMenu::MenuPetal::mouseEnter(const MouseEvent& e)
    {
        makeSelected(true);
    }

    void CircularMenu::MenuPetal::mouseExit(const MouseEvent& e)
    {
        makeSelected(false);
    }

    void CircularMenu::MenuPetal::setGeneralOffset(const int x, const int y)
    {
        generalOffsetX = x;
        generalOffsetY = y;
    }

    void CircularMenu::MenuPetal::makeSelected(const bool shouldBeSelected)
    {
        if (!isPreSelected && shouldBeSelected)
        {
            baseColour = baseColour.withAlpha(1.0f);
            if (iconImage) iconImage->multiplyAllAlphas(1.0f/iconAlpha);
            repaint();
            isPreSelected = true;
        }
        else if (isPreSelected && !shouldBeSelected)
        {
            baseColour = baseColour.withAlpha(0.4f);
            if (iconImage) iconImage->multiplyAllAlphas(iconAlpha);
            repaint();
            isPreSelected = false;
        }
    }

    bool CircularMenu::MenuPetal::isSelected() { return isPreSelected; }

    //===================================== Menu
    bool CircularMenu::addItem(const int itemResultId, Drawable* iconToUse, const String& itemText)
    {
        if (!iconToUse) return false;
        if (itemsId.contains(itemResultId)) return false;
        itemsId.add(itemResultId);
        icons.add(iconToUse);
        itemTexts.add(itemText);
        return true;
    }

    int CircularMenu::show(const int minimumRadius)
    {
        // Compute the radius
        int x, y;
        Desktop::getMousePosition (x, y);


        return showAt(x, y, minimumRadius);
    }



    Component* CircularMenu::createMenuComponent (const int x, const int y, const int w, const int h, ApplicationCommandManager** managerOfChosenCommand, Component* const componentAttachedTo) throw()
    {
        CircularMenuWindow * window = new CircularMenuWindow(x, y, w, h, icons, itemsId, itemTexts, managerOfChosenCommand, componentAttachedTo);
        if (window) window->setVisible(true);
        return window;
    }


    int CircularMenu::showAt(const int screenX, const int screenY, const int minimumRadius)
    {
        // Save the previously focused item to restore after this call
        Component* const prevFocused = Component::getCurrentlyFocusedComponent();
        // Also save the top level component
        Component* const prevTopLevel = (prevFocused != 0) ? prevFocused->getTopLevelComponent() : 0;

        // We want to be informed if the previous focus component is being deleted
        ComponentDeletionWatcher* deletionChecker1 = 0;
        if (prevFocused != 0) deletionChecker1 = new ComponentDeletionWatcher (prevFocused);
        ComponentDeletionWatcher* deletionChecker2 = 0;
        if (prevTopLevel != 0) deletionChecker2 = new ComponentDeletionWatcher (prevTopLevel);

        bool wasHiddenBecauseOfAppChange = false;

        int result = 0;
        ApplicationCommandManager* managerOfChosenCommand = 0;

        Component* const popupComp = createMenuComponent (screenX - minimumRadius, screenY - minimumRadius, minimumRadius * 2 , minimumRadius * 2,
                                                          &managerOfChosenCommand,
                                                          0);

        if (popupComp != 0)
        {
            popupComp->enterModalState (false);
            popupComp->toFront (false);  // need to do this after making it modal, or it could
                                         // be stuck behind other comps that are already modal..

            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;

        if (managerOfChosenCommand != 0 && result != 0)
        {
            ApplicationCommandTarget::InvocationInfo info (result);
            info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;

            managerOfChosenCommand->invoke (info, true);
        }

        return result;
    }
}

#4

ooh this is rather nice! Thanks for sharing!

This reminds me of https://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=3006&lngWId=10 which I was using years back when I was developing alternative HCI, great UX!

π