ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
57.96k stars 9.93k forks source link

Unable to dock window over a dockspace inside a popup #6882

Open vdweller84 opened 9 months ago

vdweller84 commented 9 months ago

Hello,

In order to avoid any XY confusion, I'd like to describe precisely what I'm trying to accomplish:

I would like to show a fullscreen modal window. This window also serves as a dockspace. A set of other windows can be rearranged within it and interact (dock) only with it or with each other.

My goal is ultimately to block all windows below this setup and create a "main" empty modal window where other windows (property panels) can be arranged freely. Obviously this setup makes sense as a whole (imagine a 3D model editor window with several smaller property windows that can be docked around it).

I tried various configurations but I can't make anything that works. Obviously, I understand that, give the current structure of the API regarding modals/docking , this is probably a silly proposition, just wanted to make sure that this is indeed the case.

ocornut commented 9 months ago

Modal windows can absolutely host a dockspace if you call DockSpace() within it. I am not sure what your problem is precisely.

vdweller84 commented 9 months ago

@ocornut I am aware that modals can host a dockspace. My question is:

Consider a modal window A which hosts a dockspace.

Can there be non-modal windows B, C and D above that window A, that can exist separately from A but can also be docked inside A ?

image

ocornut commented 9 months ago

Can there be non-modal windows B, C and D above that window A

Yes if they are submitted from within the Begin stack of A.

that can exist separately from A

I don’t understand what that means.

You seem to be frequently cornerning yourself into complex and unusual problems. I would question your approach. The purpose of a modal goes hand in hand with the idea that they are short lived. There’s little point in letting the user configure the docking state of a short lived window.

vdweller84 commented 9 months ago

that can exist separately from A

I meant that non-modals B and C are not just pre-docked inside modal A, but they can also be un-docked above it.

The reason I am concerned with more complex setups is both that I have encountered these setups in other editing software and also that they serve a specific purpose.

There are many programs that use complex modals with an arbitrary lifespan like VST plugins in audio editing, room/scene editors in 2D/3D game engines etc.

Modals with complex setups are used for blocking interaction with underlying windows while users perform work that requires interaction with various other property panels - maybe I don't want the user to start deleting assets that I am directly using in that specific editor window.

On the topic at hand: The closest I have come to docking windows in a modal is this:

        static ImGuiWindowClass windowClass;

        static ImGuiID dockspaceID = 0;

        ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Once);
        ImGui::SetNextWindowSize(viewport->WorkSize, ImGuiCond_Once);

        ImGui::BeginPopupModal(name.c_str(), 0, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus);

        if (!ImGui::DockBuilderGetNode(dockspaceID)) {
            dockspaceID = ImGui::GetID(name.c_str());
            ImGui::DockBuilderRemoveNode(dockspaceID);
            ImGui::DockBuilderAddNode(dockspaceID, ImGuiDockNodeFlags_DockSpace);
            ImGui::DockBuilderSetNodeSize(dockspaceID, ImGui::GetContentRegionAvail());
            ImGui::DockBuilderFinish(dockspaceID);
        }

        ImGui::DockSpace(dockspaceID);

        windowClass.ClassId = dockspaceID;

        //LEFT PANEL
        ImGui::SetNextWindowClass(&windowClass);
        ImGui::SetNextWindowSize(ImVec2(400,300), ImGuiCond_Once);
        if (ImGui::Begin("B")) {

        }
        ImGui::End();

        //RIGHT PANEL
        ImGui::SetNextWindowClass(&windowClass);
        ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_Once);
        if (ImGui::Begin("C")) {
            //...
        }
        ImGui::End();

        ImGui::EndPopup();

I can dock window B or C inside modal A, but the remaining window can't be docked whatsoever.

I know that several "key" steps are missing (like SetNextWindowDockID()) but results are even more...uuuhhhh...not functional with them.

I would like some direction towards a working setup. If this remains highly unusual / complex / not worth time, I will close this and work towards simpler designs.

ocornut commented 9 months ago

I meant that non-modals B and C are not just pre-docked inside modal A, but they can also be un-docked above it.

Yes that’ll work.

I can dock window B or C inside modal A, but the remaining window can't be docked whatsoever.

Which remaining window ?

I would like some direction towards a working setup.

I am not sure what your question is, the code you posted should be working ?

I know that several "key" steps are missing (like SetNextWindowDockID())

If you are using dock builder i see no reason for bothering to use SetNextWindowDockID().

Also not sure if it is because some code has been stripped out, but i’m not sure your use of windowclass has any effect given the context.

Modals with complex setups are used for blocking interaction with underlying windows while users perform work that requires interaction with various other property panels - maybe I don't want the user to start deleting assets that I am directly using in that specific editor window.

Note that you can achieve that by, well, not submitting the other/underlying contents. If you change the contents you display thats also a perfectly fine way of achieving modality.

vdweller84 commented 9 months ago

A few clarifications on the code I posted:

Which remaining window ?

With the code as it is, I can dock non-modal B into modal A or non-modal C into modal A but not both.

the code you posted should be working ?

See above.

i’m not sure your use of windowclass has any effect given the context

Commenting SetNextWindowClass() lines causes neither of the B, C windows be able to dock into modal A. Like, it's as if docking was never enabled in the first place. I don't know why.

Note that you can achieve that by, well, not submitting the other/underlying contents. If you change the contents you display thats also a perfectly fine way of achieving modality.

That is an interesting point and one I will take into account if my proposed method fails to work.

ocornut commented 9 months ago

I confirm there's an issue, we have code to prevent docking over a popup in DockNodeIsDropAllowedOne():

// Prevent docking any window created above a popup
// Technically we should support it (e.g. in the case of a long-lived modal window that had fancy docking features),
// by e.g. adding a 'if (!ImGui::IsWindowWithinBeginStackOf(host_window, popup_window))' test.
// But it would requires more work on our end because the dock host windows is technically created in NewFrame()
// and our ->ParentXXX and ->RootXXX pointers inside windows are currently mislading or lacking.
ImGuiContext& g = *GImGui;
for (int i = g.OpenPopupStack.Size - 1; i >= 0; i--)
    if (ImGuiWindow* popup_window = g.OpenPopupStack[i].Window)
        if (ImGui::IsWindowWithinBeginStackOf(payload, popup_window))   // Payload is created from within a popup begin stack.
            return false;

Here's compilable repro:

#include "imgui_internal.h" // DockBuilderXXX

void Test6882()
{
    if (ImGui::Button("Open Modal"))
        ImGui::OpenPopup("modal");

    static ImGuiID dockspaceID = 0;

    ImGuiViewport* viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Once);
    ImGui::SetNextWindowSize(viewport->WorkSize, ImGuiCond_Once);

    if (!ImGui::BeginPopupModal("modal", 0, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus))
        return;

    if (!ImGui::DockBuilderGetNode(dockspaceID))
    {
        dockspaceID = ImGui::GetID("dockspacename");
        ImGui::DockBuilderRemoveNode(dockspaceID);
        ImGui::DockBuilderAddNode(dockspaceID, ImGuiDockNodeFlags_DockSpace);
        ImGui::DockBuilderSetNodeSize(dockspaceID, ImGui::GetContentRegionAvail());
        ImGui::DockBuilderFinish(dockspaceID);
    }

    ImGui::DockSpace(dockspaceID);

    ImGuiWindowClass windowClass;
    windowClass.ClassId = ImGui::GetID("classname");

    //LEFT PANEL
    //ImGui::SetNextWindowClass(&windowClass);
    ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_Once);
    if (ImGui::Begin("B")) {

    }
    ImGui::End();

    //RIGHT PANEL
    //ImGui::SetNextWindowClass(&windowClass);
    ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_Once);
    if (ImGui::Begin("C")) {
        //...
    }
    ImGui::End();

    ImGui::EndPopup();
}
ocornut commented 9 months ago

i’m not sure your use of windowclass has any effect given the context Commenting SetNextWindowClass() lines causes neither of the B, C windows be able to dock into modal A. Like, it's as if docking was never enabled in the first place. I don't know why.

I was puzzled by this but it turns out it was a bug in DockNodeIsDropAllowedOne(): when we added the second set of filters in that function we forgot to notice how the first sets of filter (the ClassId ones) eagerly returned true immediately. So it was a bug that messing with ClassId allowed docking here. That part of fixed by ff534b0.

As for the general issue of preventing to dock over a popup, I don't think I have the bandwidth to investigate this soon but at first glance adding the line suggested in comment makes it possible.

ocornut commented 9 months ago

There's also a third issue: the use of ImGuiWindowFlags_NoBringToFrontOnFocus on a modal leads to an inconsistent setup: the window never gets added to the front of Z order and previously visible windows which should be "under" and are unusable still appear over.

(A) One approach would be to modify the CreateNewWindow() to make the window appear in the display order above every window that are under it in the begin-stack. I think would be the correct symmetrical action to what's done in FocusWIndow() (trying to focus a window under the window will move it right below the modal).

(B) Another approach would be to consider removing the ImGuiWindowFlags_NoBringToFrontOnFocus and assuming that floating windows created within the modal stack to be necessarily above it. But that would be a restrictive design: we currently allow those floating windows to be behind it for the simple reason that you can have a movable and non-fullscreen modal. But because yours is fullscreen and non-moveable, it become impossible to have something under, so it's not a possible change of design.

You'd probably need at least 1 and 3 to make this workable.

That is an interesting point and one I will take into account if my proposed method fails to work.

Honestly if your modal is fullscreen and non-moveable it makes little sense to not do that. It's much much simpler.

vdweller84 commented 9 months ago

Thanks for investigating.

I am trying to leave not submitting anything else as a last resort since this involves adding checks to all other windows.

I have also tried my luck with a non-modal, immovable, fullscreen window:

        static ImGuiID dockspaceID = 0;

        ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Once);
        ImGui::SetNextWindowSize(viewport->WorkSize, ImGuiCond_Once);

        ImGuiWindowFlags wFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse;

        ImGui::Begin(name.c_str(), 0, wFlags);

        if (!ImGui::DockBuilderGetNode(dockspaceID)) {
            dockspaceID = ImGui::GetID(name.c_str());
            ImGui::DockBuilderRemoveNode(dockspaceID);
            ImGui::DockBuilderAddNode(dockspaceID, ImGuiDockNodeFlags_DockSpace);
            ImGui::DockBuilderSetNodeSize(dockspaceID, ImGui::GetContentRegionAvail());
            ImGui::DockBuilderFinish(dockspaceID);
        }

        ImGui::DockSpace(dockspaceID);

        //LEFT PANEL
        ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_Once);
        if (ImGui::Begin("B")) {

        }
        ImGui::End();

        //RIGHT PANEL
        ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_Once);
        if (ImGui::Begin("C")) {

        }
        ImGui::End();

        ImGui::End();

There are a few noteworthy points here:

I haven't found any way to prevent docking in A's title bar. I don't even want this to be able to happen.

This is basically the main question of this post: Can I disable docking window B or C into A's title bar? If not, can docked windows B or C respect A's immovability (ie dragging their title bars does nothing - though they still can be undocked). Of course, properly implementing docking in modal windows would probably render this issue irrelevant.

        ImGuiWindowFlags wFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse;

        static int k = 0;
        ++k;
        if (k >= 2)
            wFlags |= ImGuiWindowFlags_NoBringToFrontOnFocus;

        ImGui::Begin(name.c_str(), 0, wFlags);

But I am not entirely convinced of its robustness.

This leaves me with the question of my first point: How can I disable docking in a window's title bar?

EDIT: A temp workaround is to set Window A's size and pos every frame:

        ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->Pos);
        ImGui::SetNextWindowSize(viewport->Size);

This causes any window being docked in A's title bar to just become "maximized", ie cover the whole screen without being docked in A's title bar.