Closed Per-Morten closed 12 months ago
Thirdly, (not very important) This approach doesn't feel super "intended" but more "workaroundy". Using state storage to store "single frame behaviour" isn't something I normally need to do in ImGui. Now every begin/end need to have this "little dance" of using state storage that's modified inside the window. Note, so far I've been able to make all my tools with a minimal amount of abstractions/utilities on top of Dear ImGui, so this feeling might just be lack of more complex experience.
So I at least figured out that I can avoid using state storage by using begin/end to get into the context of the window https://github.com/ocornut/imgui/wiki/Tips. Only really solves the third issue (easier to abstract away in a function). But, is this allowed, am I allowed to set the window dock id after I've already created the window once?
{
ImGui::SetNextWindowClass(&window_class);
char window_a_heading[256];
std::sprintf(window_a_heading, "Item A##%d", dockspace_id);
// Create window just to check if we must be reparented.
bool must_be_parented = false;
if (ImGui::Begin(window_a_heading))
must_be_parented = ImGui::GetWindowDockID() == 0 && !ImGui::IsKeyDown(ImGuiKey_LeftShift);
ImGui::End();
if (must_be_parented)
ImGui::SetNextWindowDockID(dockspace_id);
if (ImGui::Begin(window_a_heading))
{
// ...
}
ImGui::End();
}
After some experimentation I finally found an approach that gives me the functionality I wanted. In case anyone is trying to do something similar, this is what I ended up with:
ImGuiID ForcedDockSpace(ImGuiID id, ImGuiWindowClass* window_class, const ImVec2& size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0)
{
window_class->DockingAllowUnclassed = false;
if (!ImGui::IsKeyDown(ImGuiKey_LeftShift))
flags |= ImGuiDockNodeFlags_NoUndocking;
return ImGui::DockSpace(id, ImVec2(0.0f, 0.0f), flags, window_class);
}
bool BeginForceDockedWindow(const char* name, ImGuiID dockspace_id, const ImGuiWindowClass* window_class = NULL, bool* p_open = NULL, ImGuiWindowFlags flags = 0)
{
if (window_class)
ImGui::SetNextWindowClass(window_class);
// Need an id for state storage. Can't use window->GetID, because even
// if window has been created before it might have had its memory
// compacted, meaning that the IDStack is empty. IDStack is recreated in
// ImGui::Begin. Instead of recreating it we just create our own id we
// can use in state storage. State storage is local per window, so we
// shouldn't need this id to be seeded by the window.
const ImGuiID check_force_dock_on_frame_id = ImHashStr("ForceDockedWindow::CheckForceDockOnFrame");
const ImGuiID drag_drop_previous_frame_id = ImHashStr("ForceDockedWindow::DragDropPreviousFrame");
bool force_dock = false;
ImGuiWindow* window = ImGui::FindWindowByName(name);
if (window)
{
int check_on_frame = window->StateStorage.GetInt(check_force_dock_on_frame_id, 0);
if (ImGui::GetFrameCount() == check_on_frame)
{
ImGuiContext* ctx = ImGui::GetCurrentContext();
ImGuiDockNode* parent_node = ImGui::DockContextFindNodeByID(ctx, dockspace_id);
force_dock = !ImGui::DockNodeIsInHierarchyOf(window->DockNode, parent_node);
// If we let go of shift but not the mouse button while in drag
// and drop mode we'll move the root window to the position of
// the mouse unless we clear the ActiveID
if (ctx->ActiveIdWindow == window)
ImGui::ClearActiveID();
}
}
ImGui::SetNextWindowDockID(dockspace_id, force_dock ? ImGuiCond_Always : ImGuiCond_FirstUseEver);
bool begin_result = ImGui::Begin(name, p_open, flags);
window = ImGui::GetCurrentWindow();
bool* drag_drop_previous_frame = window->StateStorage.GetBoolRef(drag_drop_previous_frame_id, false);
bool drag_drop_current_frame = ImGui::IsDragDropActive();
bool ended_docking_this_frame = *drag_drop_previous_frame && !drag_drop_current_frame;
*drag_drop_previous_frame = drag_drop_current_frame;
if (ended_docking_this_frame)
window->StateStorage.SetInt(check_force_dock_on_frame_id, ImGui::GetFrameCount() + 1);
return begin_result;
}
void EndForceDockedWindow()
{
ImGui::End();
}
Which can be used like this:
if (ImGui::Begin("Root A"))
{
ImGuiWindowClass window_class;
window_class.ClassId = ImGui::GetID("##WindowClass");
window_class.DockingAllowUnclassed = false;
auto dockspace_id = ForcedDockSpace(ImGui::GetID("##ForcedDockSpace"), &window_class);
if (BeginForceDockedWindow("Child A", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
if (BeginForceDockedWindow("Child B", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
if (BeginForceDockedWindow("Child C", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
}
ImGui::End();
Sorry for not looking into this before, quite swamped in things to do.
Firstly, it doesn't work if ImGuiWindowClass::DockingAlwaysTabBar is set to true. Because then the item windows creates their own docking space when they are undocked from the root windows.
This statement may be incorrect, as my own demo for it (https://github.com/ocornut/imgui/issues/6487#issuecomment-1580720798) doesn't exhibit the issue. Just tested now and activated DockingAlwaysTabBar = true
and the filtering worked.
If I understand correctly, your main issue is that you never want those "Item" windows to be kept floating outside, unless they being dragged?
If you only want to prevent them from being docked into another "Root" window, then SetNextWindowClass()
with a ClassId
value should be enough and anything else is extraneous.
If you absolutely need to "reparent" the "Item" window to their "Root" window if they are dropped outside, what I would do is the following:
SetNextWindowClass()
constraint above.GetWindowDockID()
after Begin() and IF the value is not 0, record it somewhere (each item records it's last non-0 dock id).IsItemActive()
after Begin() to tell if a window is being dragged. When that's false and GetWindowDockID() == 0
) you may restore the last valid value using DockBuilderDockWindow()
or SetNextWindowDockID() + re Begin()
.// Create window class so we can't be docked to docking zones in // other windows I assume this is because the window_class will have // a ClassId that's affected by the ID of Root A? window_class.ClassId = ImGui::GetID("##WindowClass");
For info: that's correct. The ID stack of each window include the window name at bottom of the stack, so here your class id would actually be == ImHashStr("Root A##WindowClass");
Sorry for not looking into this before, quite swamped in things to do.
No worries, I closed the issue mainly because I found a working solution. It's not very important for me to improve on that solution unless there's a much simpler way of doing it (or one that doesn't require accessing imgui_internal.h, so there's less chance of it breaking in updates). So feel free to leave this issue closed and not following up on it further :)
Firstly, it doesn't work if ImGuiWindowClass::DockingAlwaysTabBar is set to true. Because then the item windows creates their own docking space when they are undocked from the root windows.
This statement may be incorrect, as my own demo for it (#6487 (comment)) doesn't exhibit the issue. Just tested now and activated
DockingAlwaysTabBar = true
and the filtering worked.
I see I forgot to include a gif of that behaviour but it is present when I run the code I originally posted in the DirectX11 example. (only modified io.ConfigDockingWithShift to be true, since that's kinda required for the original code to work as intended.
If I understand correctly, your main issue is that you never want those "Item" windows to be kept floating outside, unless they being dragged?
Yes. Essentially, the user can layout (split, merge, etc) those "Item" windows as much as they want inside of a specific dock node/window.
If you only want to prevent them from being docked into another "Root" window, then
SetNextWindowClass()
with aClassId
value should be enough and anything else is extraneous.If you absolutely need to "reparent" the "Item" window to their "Root" window if they are dropped outside, what I would do is the following:
* Setup the regular `SetNextWindowClass()` constraint above. * Call `GetWindowDockID()` after Begin() and IF the value is not 0, record it somewhere (each item records it's last non-0 dock id). * You can call `IsItemActive()` after Begin() to tell if a window is being dragged. When that's false and `GetWindowDockID() == 0`) you may restore the last valid value using `DockBuilderDockWindow()` or `SetNextWindowDockID() + re Begin()`.
So, something like this?
bool
BeginForceDockedWindow(const char* name, ImGuiID root_dockspace_id, const ImGuiWindowClass* window_class = nullptr, bool* p_open = nullptr, ImGuiWindowFlags flags = 0)
{
if (window_class)
ImGui::SetNextWindowClass(window_class);
ImGui::Begin(name, p_open, flags);
const bool is_window_dragged = ImGui::IsItemActive();
const int current_dock_id = ImGui::GetWindowDockID();
int* previous_valid_dock_id = ImGui::GetStateStorage()->GetIntRef(ImGui::GetID("PreviousValidDockID"), root_dockspace_id);
// Validate current_dock_id is within hierarchy of root_dockspace_id
bool is_in_correct_hierarchy = false;
ImGuiDockNode* itr = ImGui::GetWindowDockNode();
while (itr)
{
if (itr->ID == root_dockspace_id)
{
is_in_correct_hierarchy = true;
break;
}
itr = itr->ParentNode;
}
*previous_valid_dock_id = is_in_correct_hierarchy ? current_dock_id : root_dockspace_id;
const int valid_dock_id = *previous_valid_dock_id;
ImGui::End();
if (!is_window_dragged && !is_in_correct_hierarchy)
ImGui::SetNextWindowDockID(valid_dock_id);
bool ret = ImGui::Begin(name, p_open, flags);
return ret;
}
void EndForceDockedWindow()
{
ImGui::End();
}
// Use
if (ImGui::Begin("Root A"))
{
auto window_class = ImGuiWindowClass();
window_class.ClassId = ImGui::GetID("##WindowClass");
window_class.DockingAllowUnclassed = false;
static bool always_tab_bar = false;
ImGui::Checkbox("Always tab bar", &always_tab_bar);
window_class.DockingAlwaysTabBar = always_tab_bar;
auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);
if (BeginForceDockedWindow("Item A", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
if (BeginForceDockedWindow("Item B", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
}
ImGui::End();
if (ImGui::Begin("Root B"))
{
auto window_class = ImGuiWindowClass();
window_class.ClassId = ImGui::GetID("##WindowClass");
window_class.DockingAllowUnclassed = false;
static bool always_tab_bar = false;
ImGui::Checkbox("Always tab bar", &always_tab_bar);
window_class.DockingAlwaysTabBar = always_tab_bar;
auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);
if (BeginForceDockedWindow("Item C", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
if (BeginForceDockedWindow("Item D", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
}
ImGui::End();
Somewhat an improvement, less state, and I don't need to require the user to hold shift to drag & drop windows etc. However, when DockingAlwaysTabBar is set to true, I trigger an assert in Begin, line 6840.
// Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack
ImGuiWindow* parent_window_in_stack = (window->DockIsActive && window->DockNode->HostWindow) ? window->DockNode->HostWindow : g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window;
ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow;
IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow));
Parent window ends up being null, while ImGuiWindowFlags_ChildWindow is set on flags (set from ImGui::Begin, I'm not setting any window flags).
You shouldn’t have to validate the hierarchy, the ClassId system should do that for you.
Ok, then I think I'm misunderstanding something, or we are potentially talking past each other. If I understand your approach correctly, and drop the hierarchy validation, then I'm left with this:
bool
BeginForceDockedWindow(const char* name, ImGuiID root_dockspace_id, const ImGuiWindowClass* window_class = nullptr, bool* p_open = nullptr, ImGuiWindowFlags flags = 0)
{
if (window_class)
ImGui::SetNextWindowClass(window_class);
ImGui::Begin(name, p_open, flags);
const bool is_item_active = ImGui::IsItemActive();
const int current_dock_id = ImGui::GetWindowDockID();
int* previous_valid_dock_id = ImGui::GetStateStorage()->GetIntRef(ImGui::GetID("PreviousValidDockID"), root_dockspace_id);
if (current_dock_id != 0)
*previous_valid_dock_id = current_dock_id;
ImGui::End();
if (is_item_active == false && current_dock_id == 0)
ImGui::SetNextWindowDockID(*previous_valid_dock_id);
bool ret = ImGui::Begin(name, p_open, flags);
ImGui::Text("%d", ImGui::GetWindowDockID()); // debug
return ret;
}
void EndForceDockedWindow()
{
ImGui::End();
}
// Usage
if (ImGui::Begin("Root A"))
{
auto window_class = ImGuiWindowClass();
window_class.ClassId = ImGui::GetID("##WindowClass");
window_class.DockingAllowUnclassed = false;
static bool always_tab_bar = false;
ImGui::Checkbox("Always tab bar", &always_tab_bar);
window_class.DockingAlwaysTabBar = always_tab_bar;
auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);
if (BeginForceDockedWindow("Item A", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
if (BeginForceDockedWindow("Item B", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
}
ImGui::End();
if (ImGui::Begin("Root B"))
{
auto window_class = ImGuiWindowClass();
window_class.ClassId = ImGui::GetID("##WindowClass");
window_class.DockingAllowUnclassed = false;
static bool always_tab_bar = false;
ImGui::Checkbox("Always tab bar", &always_tab_bar);
window_class.DockingAlwaysTabBar = always_tab_bar;
auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);
if (BeginForceDockedWindow("Item C", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
if (BeginForceDockedWindow("Item D", dockspace_id, &window_class))
{
}
EndForceDockedWindow();
}
ImGui::End();
That works for the situation where the item windows are docked on the same node.
But it does not work when I've created the vertical split before I pull Item C out to "float". At the end of the animation Item C should have snapped back like it did in the previous gif, (or snapped back to it's vertical split layout, but I assume that's more complicated, so it's not important for me)
And it does not work when always tab bar is enabled.
```cpp
#include "imgui.h"
#include "imgui_impl_win32.h"
#include "imgui_impl_dx11.h"
#include
Version/Branch of Dear ImGui: Version: 1.90.0 Branch: docking
Back-end/Renderer/Compiler/OS Back-ends: imgui_impl_win32 + imgui_impl_dx11 Operating System: Windows 11 x64
My Issue/Question: I'm creating a "file inspector" debug tool, where each root window (Root A and Root B in the gifs) is a "file". Each of these files can have multiple items (Item A->Item D in the gifs) which is an object within said file. I want to be able to split/layout all the different objects within a file, but I never want those those objects to be docked to anything other than their original windows (to avoid confusion as to what file an object belongs to).
Basically, to summarize the functionality I want (see gif): I want Item A and Item B (and any other items I would add to Root A) to be able to be split into all endless combinations that docking allow, but they must always be docked somewhere in their original dockspace in the Root A window.
I'm wondering what's the best way to achieve this functionality?
My current approach (see code snippet) is to only allow layout changes (undocking) when the user is holding the shift key. If the user lets go of the shift key and the window is not docked to its original dock space I force it back there. I'm using StateStorage to track whether or not the window needs to be docked in its original dock space.
This approach works, but has some issues: Firstly, it doesn't work if ImGuiWindowClass::DockingAlwaysTabBar is set to true. Because then the item windows creates their own docking space when they are undocked from the root windows.
Secondly, If you have an undocked root, are in the process of moving an item window and let go of shift, the root is moved to where your mouse cursor is (see second gif showing the difference in behaviour).
Thirdly, (not very important) This approach doesn't feel super "intended" but more "workaroundy". Using state storage to store "single frame behaviour" isn't something I normally need to do in ImGui. Now every begin/end need to have this "little dance" of using state storage that's modified inside the window. Note, so far I've been able to make all my tools with a minimal amount of abstractions/utilities on top of Dear ImGui, so this feeling might just be lack of more complex experience.
To somewhat repeat my question: Is there any better way to achieve the functionality that I want?
Screenshots/Video Gif 1: Showing current & desired behaviour
Gif 2: Showing the broken move behaviour
Standalone, minimal, complete and verifiable example: