[Bug] Minimize to dock on MacOS causes modal component to lose modality

If you create and install a child component over your mainComponent, and set it to be modal (so as to simulate a modal dialog for example), if you then minimize that state to the dock and restore it, the modal component is no longer modal.

Test Project:

ModalComponent.zip (11.4 KB)

Steps to reproduce:

  1. In the MainWindow, click the “Modal Component” button. This installs the child component and calls enterModalState(). The modal component comes up resembling a dialog. Test modality by trying to activate any buttons in the background mainComponent. Cannot do so - it is modal.

  2. Click on the minimize button in the title bar to minimize it to the dock.

  3. Restore it from the dock. Test modality - it is now gone and you can freely click buttons in the background mainComponent.

What happens is:
When the MainWindow is minimized, in ModalComponentManager::ModalItem, the ComponentMovementWatcher that is attached to each modal component receives a componentVisibilityChanged:

    void componentVisibilityChanged() override
    {
        if (! component->isShowing())
            cancel();
    }

This then causes the active flag for that component to be set to false, and an asyncUpdate is triggered:

    void cancel()
    {
        if (isActive)
        {
            isActive = false;

            if (auto* mcm = ModalComponentManager::getInstanceWithoutCreating())
                mcm->triggerAsyncUpdate();
        }
    }

handleAsyncUpdate then removes the component from the stack of modal components. Then when you unminimize, the component is no longer modal:

void ModalComponentManager::handleAsyncUpdate()
{
    for (int i = stack.size(); --i >= 0;)
    {
        auto* item = stack.getUnchecked (i);

        if (! item->isActive)
        {
            std::unique_ptr<ModalItem> deleter (stack.removeAndReturn (i));
            Component::SafePointer<Component> compToDelete (item->autoDelete ? item->component : nullptr);

            for (int j = item->callbacks.size(); --j >= 0;)
                item->callbacks.getUnchecked (j)->modalStateFinished (item->returnValue);

            compToDelete.deleteAndZero();
        }
    }
}

Tested with JUCE 7.0.5 and Mojave. I did not test on Windows but from the code, I expect similar behavior.

1 Like

(Bump)

This seems to be an issue ONLY on MacOS, and only for a GUI app. I have now tested this project on Windows, and also as a Mac and Windows VST3 plugin.

On Windows (GUI App) - when the component is modal, you cannot click on the minimize or close buttons in the titlebar. They are disabled.

But on MacOS, you can click the minimize and close buttons, with a modal component. Why is this?

There’s no good way to control the disabled state of the minimize button on the Mac with a native titlebar, either. So you can’t prevent it from being used.

In the AudioPluginHost, in both Mac and Windows, clicking the minimize button on the PluginEditor with the modal component up is allowed, but results in no visibility change coming to the modal component, so the problem does not exist. When you unminimize, the component is still modal.

It appears the Mac version needs to be smarter about whether the top desktop window is being minimized or not, and provide some sort of exception for this. Something like this, which works, but I’m not sure how robust it would be…

struct ModalComponentManager::ModalItem  : public ComponentMovementWatcher
{
    [snip...]

    void componentVisibilityChanged() override
    {
        if (! component->isShowing())
        {
            #if JUCE_MAC
            if (component->getTopLevelComponent()->isOnDesktop())
            {
                if (component->getPeer()->isMinimised())
                {
                    DBG("**    ModalItem::componentVisibilityChanged: " + component->getName() + " - SKIP Deletion");
                    return;
                }
            }
            #endif //JUCE_MAC

            cancel();
        }
    }

I also just tested with 7.0.7 develop latest, and on a newer MacOS (Big Sur at least), and the issue is still there.

@reuk - this is precisely what I am doing - using a component overlay on my MainEditor, along with EnterModalState(). While this is working fine, and minimizing the window causes no issues in a plugin on either Windows or Mac, the same is not true for my GUI App version of the same thing. On MacOS, minimizing the window causes a loss of modality as explained above.

I agree this is not ideal, and a difference compared to the Windows behaviour.

I’m considering the options here. The suggested workaround could break existing use-cases. In an application with multiple windows, minimising the one with the modal component would leave the visible window in a non-interactive state, and it would be difficult to discover why this is the case.

Could you handle your particular use-case satisfactorily with the current JUCE API? You can receive a callback when the modal state is cancelled. Using ComponentMovementWatcher::componentVisibilityChanged() you could detect when the component becomes visible again and restore modal state if it is still require.

You might want to prevent the window from being minimised if a modal state is active, like on Windows. That could probably be added to JUCE for non-plugin windows. Would this be a complete solution in your situation?

1 Like

Thanks for looking at this. I agree the suggested fix would not be ideal.

That seems a rather complicated workaround. I just took a look at it, and wouldn’t you need to restore the modality by calling enterModalState() with the same callback that you originally used? And that means you couldn’t do it with a lambda in place, you’d have to store the callback pointer or something… In my app I have the ability to “layer” modal dialogs, so there can be more than one up when the window is minimized… wow, seems it would be a lot of rewriting and testing…

Personally, I’d prefer the Windows and Mac behaviors to be identical. But if that is not possible…

(I will say that on the Mac, some apps allow you to minimize with a modal open. For example, Photoshop does not. MS Word does…)

It would be great if there were some way to disable the minimize and close buttons on MacOS (with a native title bar) at your discretion.

Or some way to ignore the minimize button. For now, on the close button, I override DocumentWindow::closeButtonPressed() so that, even if not disabled from being clicked while a modal is up, I can just ignore it. However, there does not seem to be an equivalent for the minimize button. I remember searching for quite a bit so if I missed it, let me know.

A change has been released on develop

The minimise and close buttons are now disabled when a component is modal, to avoid this problem the same way Windows does.

3 Likes

Here’s a related case. On Windows, if you press the Windows+D key (to minimize all windows), it’ll minimize your window even if it’s in a modal state, ignoring whether the minimize button in the window header is disabled. So, for example, our plugin draws its setting dialog as a component overlaid over the main UI, and then calls enterModalState. If this is open and then you press the Windows+D key to minimize all windows, then you can never re-open the UI (by clicking its icon on the taskbar, or alt-tabbing to it).
Any suggestions?

1 Like

*note, you can re-open the window by pressing Windows+D again. but you should also be able to re-open the window by clicking on its taskbar icon, or alt-tab

In the TopLevelModalDismissBroadcaster in juce_Windowing_windows.cpp, there’s a processMessage function containing the following block:

if (info->message == WM_WINDOWPOSCHANGING)
{
    const auto* windowPos = reinterpret_cast<const WINDOWPOS*> (info->lParam);
    const auto windowPosFlags = windowPos->flags;

    constexpr auto maskToCheck = SWP_NOMOVE | SWP_NOSIZE;

    if ((windowPosFlags & maskToCheck) == maskToCheck)
        return;
}

Please could you change the second if as follows, and check whether the problem is resolved?

if ((windowPosFlags & maskToCheck) == maskToCheck || (windowPosFlags & 0x8000) != 0)

@reuk - yes that modification resolves the issue!
Thanks for looking into it!
-John

Thanks for confirming. Hopefully we’ll be able to get that fix merged next week.

This change is now available on the develop branch: