PopupMenu and dropshadow when not on desktop

Quick question, perhaps there’s an obvious answer that I’ve missed…

If a PopupMenu is added to the desktop, we get a nice dropshadow underneath it.
However if we add the PopupMenu to a parent component (with PopupMenu::Options()) there is no such drop shadow.

I’ve been trying to add a DropShadower to the component(s) generated by PopupMenu but run in to all sorts of issues with managing the lifetime of the DropShadower. The fact that these components are not accessible isn’t helping either.

Is there an easy way to add the dropshadow to a PopupMenu if it’s not on the desktop? Or perhaps PopupMenu can be modified such that a DropShadower is added to its menu components whenever the flag for a drop shadow is set from the LookAndFeel?

Thanks!

There might be other ways, but you could do that in your look and feel.

You would have to set the backgroundColourId of your popup to be transparent (otherwise its window will be set as opaque) :
setColour (PopupMenu::backgroundColourId, Colours::transparentWhite);

then you could draw a drop shadow in your popup background like that for example (untested):

void drawPopupMenuBackground (Graphics& g, int width, int height) override
{
    juce::Rectangle<int> area (width, height);
    DropShadow shadow;
    shadow.radius = 10;
    area = area.reduced (2 * shadow.radius);
    shadow.drawForRectangle (g, area);

    g.setColour (Colours::white); // the actual background colour you want
    g.fillRect (area);
}

Thanks! I see how that could work. However with that approach the shadow needs to fit within the clipping region of the Graphics context, and hence the menu itself needs to be reduced in size (which has implications for all other LookAndFeel methods for the PopupMenu).

Perhaps there is another way to add the drop shadow outside the clipping region, like a DropShadower does by adding a new component that ‘follows’ its owner?

Bump, I’m also curious if there is an easy way to add the drop shadow outside of the clipping region. Anyone know?

I managed to do this by changing a custom LookAndFeel. Basically reducing the size of the popup menu to make space around it to draw custom drop shadows as suggested by lalala. It works fine, with the drawback that this carved out space for the drop shadow causes sub menus to disconnect from their main menu.

It would still be good if there would be a better solution for this in JUCE (e.g. allowing to set a custom drop shadow for popup menus when adding the menu to another component). In particular for IOS, where you can’t put any popup menu on the desktop and they’ll have to live somewhere inside one of the other components…

Yeah, I’ve tried the custom LookAndFeel, but like you mention the submenus seemingly float off of the side of the parent menu, which isn’t an acceptable solution for us.

I’m with you that the best solution would be something built into JUCE (perhaps a DropShadower class could be attached to each MenuWiindow that is created). JUCE team, any thoughts?

Yeah, it seems like a sensible suggestion, but the caveat is that it’ll be both rather time consuming to get right and low priority work in comparison to a lot of other things on our wish list.

If you can make a feature request out of it and gather enough community support that would be a way to increase it’s importance.

Seems like nothing’s been done about this, I had the same problem.

I thought I would share a little hack I developed for this issue. Not sure I’m going to use it, but it seems to work.

This does involve modifying the JUCE code. What it does is, when the parentComponent is specified, is put the menuWindow into a wrapper component that is then expanded enough to draw a dropShadow. The following image (Mac) shows the normal appearance (no drop shadow), the result of the hack, and the expanded wrapper layer that is used to hold the PopupMenu shaded in green:

So, to do this, you modify juce_PopupMenu.cpp in 3 places:

At the end of struct MenuWindow : public Component, add this private struct PopupWrapper:

    // start hack ===========================
    // wrapper for PopupMenu
    struct PopupWrapper : public Component
    {
        PopupWrapper (MenuWindow& inWind)
        : menuWind (inWind)
        {
            setInterceptsMouseClicks(false, true);
            addAndMakeVisible(menuWind);
        }
        
        void paint(Graphics& g) override
        {
            // put in to see the wrapper component bounds:
//            g.fillAll(Colours::green.withAlpha(0.3f));
            
            DropShadow shadow;
            shadow.radius = shadowRadius;
            shadow.colour = Colours::black.withAlpha(0.5f);
            auto area = getLocalBounds();
            area = area.reduced (2 * shadow.radius);
            shadow.drawForRectangle (g, area);
        }
        
        void resized () override
        {
            menuWind.setTopLeftPosition(shadowRadius * 2, shadowRadius * 2);
        }
        MenuWindow& menuWind;
    };
    static const int shadowRadius = 10;
    std::unique_ptr<PopupWrapper> pw;
    // end hack ================================

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow)
};

And then, in the MenuWindow constructor, two mods near the beginning and end, to add the PopupWrapper containing the MenuWindow to the parent component, and set the bounds to an expanded version of it:

struct MenuWindow  : public Component
{
    MenuWindow (const PopupMenu& menu, MenuWindow* parentWindow,
                Options opts, bool alignToRectangle, bool shouldDismissOnMouseUp,
                ApplicationCommandManager** manager, float parentScaleFactor = 1.0f)
       : Component ("menu"),
         parent (parentWindow),
         options (std::move (opts)),
         managerOfChosenCommand (manager),
         componentAttachedTo (options.getTargetComponent()),
         dismissOnMouseUp (shouldDismissOnMouseUp),
         windowCreationTime (Time::getMillisecondCounter()),
         lastFocusedTime (windowCreationTime),
         timeEnteredCurrentChildComp (windowCreationTime),
         scaleFactor (parentWindow != nullptr ? parentScaleFactor : 1.0f)
    {
        setWantsKeyboardFocus (false);
        setMouseClickGrabsKeyboardFocus (false);
        setAlwaysOnTop (true);

        setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel())
                                          : menu.lookAndFeel.get());

        auto& lf = getLookAndFeel();

        parentComponent = lf.getParentComponentForMenuOptions (options);
        const_cast<Options&>(options) = options.withParentComponent (parentComponent);

        if (parentComponent != nullptr)
        {
            // start hack =========================
            // instead of adding the MenuWindow, add a wrapper containing the MenuWindow
            pw.reset(new PopupWrapper(*this));
            parentComponent->addAndMakeVisible (pw.get());
            //parentComponent->addChildComponent (this);
            // end hack ===========================
        }
        else
        {
            addToDesktop (ComponentPeer::windowIsTemporary
                          | ComponentPeer::windowIgnoresKeyPresses
                          | lf.getMenuWindowFlags());

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

        if (parentComponent == nullptr && parentWindow == nullptr && lf.shouldPopupMenuScaleWithTargetComponent (options))
            if (auto* targetComponent = options.getTargetComponent())
                scaleFactor = Component::getApproximateScaleFactorForComponent (targetComponent);

        setOpaque (lf.findColour (PopupMenu::backgroundColourId).isOpaque()
                     || ! Desktop::canUseSemiTransparentWindows());

        const auto initialSelectedId = options.getInitiallySelectedItemId();

        for (int i = 0; i < menu.items.size(); ++i)
        {
            auto& item = menu.items.getReference (i);

            if (i + 1 < menu.items.size() || ! item.isSeparator)
            {
                auto* child = items.add (new ItemComponent (item, options, *this));

                if (initialSelectedId != 0 && item.itemID == initialSelectedId)
                    setCurrentlyHighlightedChild (child);
            }
        }

        auto targetArea = options.getTargetScreenArea() / scaleFactor;

        calculateWindowPos (targetArea, alignToRectangle);
        setTopLeftPosition (windowPos.getPosition());
        updateYPositions();

        if (auto visibleID = options.getItemThatMustBeVisible())
        {
            auto targetPosition = parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, targetArea.getTopLeft())
                                                             : targetArea.getTopLeft();

            auto y = targetPosition.getY() - windowPos.getY();
            ensureItemIsVisible (visibleID, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1);
        }

        resizeToBestWindowPos();

        // start hack ========================
        // set the bounds of the wrapper to be expanded for the dropShadow
        if (parentComponent != nullptr)
            pw->setBounds(windowPos.expanded(shadowRadius * 2, shadowRadius * 2));
        // end hack ==========================

        getActiveWindows().add (this);
        lf.preparePopupMenuWindow (*this);

        getMouseState (Desktop::getInstance().getMainMouseSource()); // forces creation of a mouse source watcher for the main mouse
    }
1 Like