From 60c7250683b00d541c0f868231a2a82bc21b4c82 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 8 Oct 2026 21:39:31 +0200 Subject: [PATCH] feat(editor): docked layout: windows of the editor docked around the 3d view (dockspace over the main window, default layout built when there's none or on Mode windows > Reset window layout), menu shown all the time, toolbar with the edit modes, track and road windows, gizmo operations and the top view, status bar with the outcome of the last operation and the selected node; dockspace submitted also when the frame was started by the ui of another mode, so docked windows don't come undocked on the change of mode; stable ids of the editor windows, no auto resize of the dockable ones; hints of the track and road tools placed in the part of the window the 3d view is seen in; Polish translation --- application/editormode.cpp | 52 ++++++++++++++++- application/editormode.h | 2 + application/editormodegauge.cpp | 2 +- application/editormoderoad.cpp | 3 +- application/editormodetrack.cpp | 12 ++-- application/editoruilayer.cpp | 99 +++++++++++++++++++++++++++++++++ application/editoruilayer.h | 22 ++++++++ application/editoruipanels.cpp | 2 +- application/uilayer.cpp | 16 +++++- application/uilayer.h | 4 ++ lang/editor_pl.po | 6 ++ 11 files changed, 205 insertions(+), 15 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index adfb1340..73493be0 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -487,6 +487,7 @@ editor_mode::editor_mode() { // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); ui()->set_gizmo_options([this]() { render_gizmo_options(); }); + ui()->set_toolbar_options([this]() { render_toolbar(); }); ui()->set_array_options([this]() { render_array(); }); ui()->set_bend_options([this]() { render_bend(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); @@ -1423,7 +1424,7 @@ bool editor_mode::update() void editor_mode::render_settings() { - ImGui::Begin(STR_C("Editor Settings"), &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Begin((std::string(STR_C("Editor Settings")) + "###editorsettings").c_str(), &m_settings_open); if (ImGui::BeginTabBar("##editorsettings")) { @@ -2964,7 +2965,7 @@ void editor_mode::render_orthophoto_window() if (false == m_orthophoto_window) return; ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver); - if (ImGui::Begin(STR_C("Orthophoto##window"), &m_orthophoto_window, ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::Begin((std::string(STR_C("Orthophoto##window")) + "###orthophoto").c_str(), &m_orthophoto_window)) render_orthophoto_ui(); ImGui::End(); } @@ -3623,6 +3624,51 @@ void editor_mode::render_array() ImGui::PopID(); } +void editor_mode::render_toolbar() +{ + // edit modes of the node bank, each of them takes the mouse over from the track window + std::pair const modes[] = { + {STR_C("Select"), nodebank_panel::MODIFY}, {STR_C("Insert"), nodebank_panel::ADD}, {STR_C("Brush"), nodebank_panel::BRUSH}, {STR_C("Area fill"), nodebank_panel::FILL}, {STR_C("Copy to bank"), nodebank_panel::COPY}}; + for (auto const &mode : modes) + { + if (ImGui::MenuItem(mode.first, nullptr, ui()->mode() == mode.second)) + { + m_track_window_open = false; + ui()->set_mode(mode.second); + } + } + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Tracks"), nullptr, m_track_window_open)) + { + if (m_track_window_open) + m_track_window_open = false; + else + show_track_tab(m_track_tab); + } + if (ImGui::MenuItem("Roads", nullptr, m_roadtool.window)) + { + m_roadtool.window = !m_roadtool.window; + if (m_roadtool.window) + m_track_window_open = false; + } + ImGui::Separator(); + if (m_gizmo_enabled) + { + std::pair const operations[] = { + {STR_C("Translate (Q)"), gizmo_operation::translate}, {STR_C("Rotate (W)"), gizmo_operation::rotate}, {STR_C("Scale (E)"), gizmo_operation::scale}}; + for (auto const &operation : operations) + { + if (ImGui::MenuItem(operation.first, nullptr, m_gizmo_op == operation.second)) + m_gizmo_op = operation.second; + } + if (m_gizmo_op != gizmo_operation::scale && ImGui::MenuItem(STR_C("Local space (R)"), nullptr, m_gizmo_local)) + m_gizmo_local = !m_gizmo_local; + ImGui::Separator(); + } + if (ImGui::MenuItem(STR_C("Top view, orthographic (O)"), nullptr, Global.EditorOrtho)) + toggle_ortho(); +} + void editor_mode::render_gizmo_options() { if (ImGui::Button(Global.EditorOrtho ? "3D view (O)" : "Top view, orthographic (O)")) @@ -4585,7 +4631,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const void editor_mode::render_change_history(){ - ImGui::Begin(STR_C("Editor History"), &m_change_history, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Begin((std::string(STR_C("Editor History")) + "###editorhistory").c_str(), &m_change_history); int maxsize = m_max_history_size; if (ImGui::InputInt(STR_C("Max history size"), &maxsize)) { diff --git a/application/editormode.h b/application/editormode.h index 95e9bc8f..9e14f293 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -425,6 +425,8 @@ class editor_mode : public application_mode, private editor_track::observer void render_gizmo(); // gizmo settings, drawn in the toolset window void render_gizmo_options(); + // edit modes, editor windows and gizmo settings in the toolbar under the menu + void render_toolbar(); bool m_gizmo_enabled{true}; // master switch for the in-viewport gizmo bool m_gizmo_using{false}; // tracks an ongoing drag, so a single undo snapshot is taken per drag bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 2f64fb42..6d6883a0 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -708,7 +708,7 @@ void editor_mode::render_gauge_window() if (false == m_gauge.open) return; ImGui::SetNextWindowSize(ImVec2(380.0f, 420.0f), ImGuiCond_FirstUseEver); - if (false == ImGui::Begin(STR_C("Structure gauge"), &m_gauge.open)) + if (false == ImGui::Begin((std::string(STR_C("Structure gauge")) + "###structuregauge").c_str(), &m_gauge.open)) { ImGui::End(); return; diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index d66b6bcb..c7e7051c 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -2834,8 +2834,7 @@ void editor_mode::draw_road_overlay() const "LMB: select a piece, a junction, a point (white dot) or a marker"; } // shown above the place the track tools put their hints at - ImGuiIO const &io = ImGui::GetIO(); - ImVec2 const position{12.0f, io.DisplaySize.y - 56.0f}; + ImVec2 const position{editor_ui::view_min().x + 12.0f, editor_ui::view_max().y - 56.0f}; auto const size{ImGui::CalcTextSize(hint.c_str())}; drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f); drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str()); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 761b36ab..8d9f8a72 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3657,8 +3657,10 @@ void editor_mode::draw_track_hints() auto hints{track_key_hints(false)}; if (hints.size() > 4) hints.resize(4); - auto const bottom{io.DisplaySize.y}; - auto const available{io.DisplaySize.x * 0.6f - 2.0f * margin}; + // in the part of the window the 3d view is seen in, between the docked windows + auto const left{editor_ui::view_min().x + margin}; + auto const bottom{editor_ui::view_max().y}; + auto const available{(editor_ui::view_max().x - editor_ui::view_min().x) * 0.6f - 2.0f * margin}; auto const mode{track_mode_name()}; std::vector> lines(1); float used{ImGui::CalcTextSize(mode.c_str()).x + 2.0f * pad + gap}; @@ -3679,13 +3681,13 @@ void editor_mode::draw_track_hints() float width{i == 0 ? ImGui::CalcTextSize(mode.c_str()).x + 2.0f * pad : 0.0f}; for (auto const *hint : lines[i]) width += (width > 0.0f ? gap : 0.0f) + width_of(*hint); - drawlist->AddRectFilled(ImVec2(margin - 6.0f, y - pad - 2.0f), ImVec2(margin + width + 6.0f, y + lineheight - pad - 2.0f), IM_COL32(0, 0, 0, 160), 4.0f); - auto x{margin}; + drawlist->AddRectFilled(ImVec2(left - 6.0f, y - pad - 2.0f), ImVec2(left + width + 6.0f, y + lineheight - pad - 2.0f), IM_COL32(0, 0, 0, 160), 4.0f); + auto x{left}; if (i == 0) x += chip(ImVec2(x, y), mode.c_str(), IM_COL32(60, 140, 230, 235), IM_COL32(255, 255, 255, 255)); for (auto const *hint : lines[i]) { - if (x > margin) + if (x > left) x += gap; x += chip(ImVec2(x, y), hint->key, IM_COL32(255, 210, 60, 220), IM_COL32(20, 20, 20, 255)) + 5.0f; drawlist->AddText(ImVec2(x, y), IM_COL32(255, 255, 255, 230), hint->action.c_str()); diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 4f7ede62..dc38b282 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -16,6 +16,10 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "rendering/renderer.h" #include "utilities/translation.h" +#include "imgui/imgui_internal.h" + +glm::vec2 editor_ui::m_viewmin{0.0f}; +glm::vec2 editor_ui::m_viewmax{0.0f}; editor_ui::editor_ui() { @@ -29,6 +33,98 @@ editor_ui::editor_ui() m_nodebankpanel.mode_options = [this](nodebank_panel::edit_mode const Mode) { render_mode_options(Mode); }; m_nodebankpanel.header_sections = [this]() { render_header_sections(); }; + // the menu is a part of the layout of the editor, not a pop-up as in the driver mode + m_menu_always = true; +} + +void editor_ui::render_dockspace() +{ + // windows of the editor are docked around the 3d view, which is left free in the middle + auto *viewport{ImGui::GetMainViewport()}; + auto const dockspace{ImGui::GetID("###editordockspace")}; + if (m_layoutreset || ImGui::DockBuilderGetNode(dockspace) == nullptr) + { + m_layoutreset = false; + build_default_layout(dockspace); + } + ImGui::DockSpaceOverViewport(dockspace, viewport, ImGuiDockNodeFlags_PassthruCentralNode); + if (auto const *central{ImGui::DockBuilderGetCentralNode(dockspace)}; central != nullptr) + { + m_viewmin = {central->Pos.x, central->Pos.y}; + m_viewmax = {central->Pos.x + central->Size.x, central->Pos.y + central->Size.y}; + } + else + { + m_viewmin = {viewport->WorkPos.x, viewport->WorkPos.y}; + m_viewmax = {viewport->WorkPos.x + viewport->WorkSize.x, viewport->WorkPos.y + viewport->WorkSize.y}; + } +} + +void editor_ui::build_default_layout(unsigned int const Dockspace) +{ + auto *viewport{ImGui::GetMainViewport()}; + ImGui::DockBuilderRemoveNode(Dockspace); + ImGui::DockBuilderAddNode(Dockspace, ImGuiDockNodeFlags_DockSpace | ImGuiDockNodeFlags_PassthruCentralNode); + ImGui::DockBuilderSetNodePos(Dockspace, viewport->WorkPos); + ImGui::DockBuilderSetNodeSize(Dockspace, viewport->WorkSize); + // widths in pixels the current windows need, as parts of the room there is + auto const width{std::max(viewport->WorkSize.x, 1.0f)}; + auto const height{std::max(viewport->WorkSize.y, 1.0f)}; + auto const left_width{std::min(440.0f * Global.ui_scale, width * 0.3f)}; + auto const right_width{std::min(460.0f * Global.ui_scale, width * 0.3f)}; + auto const bottom_height{std::min(300.0f * Global.ui_scale, height * 0.35f)}; + ImGuiID center{Dockspace}; + auto const left{ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, left_width / width, nullptr, ¢er)}; + auto const right{ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, right_width / (width - left_width), nullptr, ¢er)}; + auto const bottom{ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, bottom_height / height, nullptr, ¢er)}; + // windows are found by the stable part of their names, after ### + for (auto const *window : {"###Toolset", "###Layers", "###scenehierarchy"}) + ImGui::DockBuilderDockWindow(window, left); + for (auto const *window : {"###trackinspector", "###roadeditor", "###editorsettings", "###Include database", "###orthophoto", "###structuregauge"}) + ImGui::DockBuilderDockWindow(window, right); + for (auto const *window : {"###profilestrip", "###editorhistory", "###modelsets"}) + ImGui::DockBuilderDockWindow(window, bottom); + ImGui::DockBuilderFinish(Dockspace); +} + +void editor_ui::render_() +{ + auto *viewport{ImGui::GetMainViewport()}; + auto const flags{ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar}; + // toolbar under the menu: the tools of the editor mode + if (ImGui::BeginViewportSideBar("##editortoolbar", viewport, ImGuiDir_Up, ImGui::GetFrameHeight(), flags)) + { + if (ImGui::BeginMenuBar()) + { + if (m_toolbaroptions) + m_toolbaroptions(); + ImGui::EndMenuBar(); + } + } + ImGui::End(); + // status bar: the outcome of the last operation, and the selected node + if (ImGui::BeginViewportSideBar("##editorstatusbar", viewport, ImGuiDir_Down, ImGui::GetFrameHeight(), flags)) + { + if (ImGui::BeginMenuBar()) + { + if (false == m_layerspanel.status.empty()) + { + if (m_layerspanel.status_error) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", m_layerspanel.status.c_str()); + else + ImGui::TextUnformatted(m_layerspanel.status.c_str()); + } + if (m_node != nullptr) + { + auto const name{m_node->name().empty() ? std::string{"(none)"} : m_node->name()}; + auto const label{std::string{STR_C("Selected:")} + " " + name}; + ImGui::SameLine(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - ImGui::CalcTextSize(label.c_str()).x - ImGui::GetStyle().ItemSpacing.x * 2.0f)); + ImGui::TextUnformatted(label.c_str()); + } + ImGui::EndMenuBar(); + } + } + ImGui::End(); } // updates state of UI elements @@ -157,6 +253,9 @@ void editor_ui::render_menu_contents() if (ImGui::BeginMenu(STR_C("Mode windows"))) { + if (ImGui::MenuItem(STR_C("Reset window layout"))) + m_layoutreset = true; + ImGui::Separator(); ImGui::MenuItem(STR_C("Toolset"), nullptr, &m_nodebankpanel.is_open); ImGui::MenuItem(STR_C("Layers"), nullptr, &m_layerspanel.is_open); ImGui::MenuItem(STR_C("Include database"), nullptr, &m_includespanel.is_open); diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f390d932..a1d4cd35 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -61,6 +61,11 @@ class editor_ui : public ui_layer { m_menuoptions = std::move(Renderer); } + // draws the tools of the editor mode in the toolbar under the menu + void set_toolbar_options(std::function Renderer) + { + m_toolbaroptions = std::move(Renderer); + } // draws the gizmo settings in the toolset window (the gizmo state lives in the editor mode) void set_gizmo_options(std::function Renderer) { @@ -83,6 +88,15 @@ class editor_ui : public ui_layer void set_file_actions(std::function Save, std::function Export); void set_new_scenery(std::function New) { m_newscenery = std::move(New); } void set_open_scenery(std::function Open) { m_openscenery = std::move(Open); } + // part of the main window between the docked windows, where the 3d view is seen; for the overlays placed in it + static glm::vec2 view_min() + { + return m_viewmin; + } + static glm::vec2 view_max() + { + return m_viewmax; + } // shows outcome of an operation in the layers window void set_status(std::string const &Status, bool const Error = false); @@ -91,6 +105,10 @@ class editor_ui : public ui_layer void render_mode_options(nodebank_panel::edit_mode const Mode); void render_header_sections(); void render_menu_contents() override; + void render_dockspace() override; + void render_() override; + // docks the windows of the editor in their default places + void build_default_layout(unsigned int const Dockspace); // members itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs @@ -106,6 +124,10 @@ class editor_ui : public ui_layer model_set_ref m_insertset; std::function m_filloptions; std::function m_menuoptions; + std::function m_toolbaroptions; + bool m_layoutreset{false}; // the default layout is to be built again on the next frame + static glm::vec2 m_viewmin; + static glm::vec2 m_viewmax; std::function m_gizmooptions; std::function m_arrayoptions; std::function m_bendoptions; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 6eae78db..a36072ed 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -892,7 +892,7 @@ void nodebank_panel::open_sets_window(int const Setid) void nodebank_panel::render_sets_window() { ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver); - if (false == ImGui::Begin(STR_C("Model sets"), &m_setsopen, ImGuiWindowFlags_NoCollapse)) + if (false == ImGui::Begin((std::string(STR_C("Model sets")) + "###modelsets").c_str(), &m_setsopen, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 4f949054..0d99ae53 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -460,6 +460,13 @@ void ui_layer::update() void ui_layer::render() { + // the frame may have been started by the ui of another mode, when the mode changes; the windows docked in the dockspace + // of this one would come undocked if they were drawn in a frame without it + if (m_dockspaceframe != ImGui::GetFrameCount()) + { + render_dockspace(); + m_dockspaceframe = ImGui::GetFrameCount(); + } render_background(); render_panels(); render_tooltip(); @@ -490,6 +497,8 @@ void ui_layer::render_internal() void ui_layer::begin_ui_frame() { begin_ui_frame_internal(); + render_dockspace(); + m_dockspaceframe = ImGui::GetFrameCount(); } void ui_layer::begin_ui_frame_internal() @@ -520,8 +529,9 @@ void ui_layer::render_hierarchy(){ if(!m_editor_hierarchy) return; - ImGui::SetNextWindowSize(ImVec2(0, 0)); - ImGui::Begin(STR_C("Scene Hierarchy"), &m_editor_hierarchy, ImGuiWindowFlags_AlwaysAutoResize); + // fitted to the contents when it first appears; no auto resize, which a docked window can't do + ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_FirstUseEver); + ImGui::Begin((std::string(STR_C("Scene Hierarchy")) + "###scenehierarchy").c_str(), &m_editor_hierarchy); ImGui::Text("Registered nodes: %zu", scene::Hierarchy.size()); ImGui::BeginChild("hierarchy_list", ImVec2(500, 300), true); @@ -683,7 +693,7 @@ void ui_layer::render_menu() { glm::dvec2 mousepos = Global.cursor_pos; - if (!((Global.ControlPicking && mousepos.y < 50.0f) || m_imguiio->WantCaptureMouse) || m_suppress_menu) + if (m_suppress_menu || (false == m_menu_always && !((Global.ControlPicking && mousepos.y < 50.0f) || m_imguiio->WantCaptureMouse))) return; if (ImGui::BeginMainMenuBar()) diff --git a/application/uilayer.h b/application/uilayer.h index 975c1c77..96cd407b 100644 --- a/application/uilayer.h +++ b/application/uilayer.h @@ -139,6 +139,8 @@ protected: virtual void render_menu_contents(); ui_log_panel m_logpanel { "Log", true }; bool m_suppress_menu = false; // if `true`, the menu at the top of the window will not be present + bool m_menu_always = false; // if `true`, the menu is shown all the time, not only when the mouse is at the top of the window + int m_dockspaceframe{-1}; // frame the dockspace was last submitted in // progress bar config float m_progress { 0.0f }; // percentage of filled progres bar, to indicate lengthy operations. float m_subtaskprogress{ 0.0f }; // percentage of filled progres bar, to indicate lengthy operations. @@ -148,6 +150,8 @@ protected: // methods // render() subclass details virtual void render_() {} + // submitted right after the start of the frame, before any window that may be docked in it + virtual void render_dockspace() {} // draws background quad with specified earlier texture void render_background(); void render_tooltip(); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 03c7c4b6..75c50c41 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -4593,3 +4593,9 @@ msgstr "Głowica na bramkę wisi nad osią toru tak wysoko nad główkami szyn" msgid " not read" msgstr " nieodczytywany" + +msgid "Reset window layout" +msgstr "Przywróć układ okien" + +msgid "Selected:" +msgstr "Zaznaczono:"