From 1bf687b4c9731c8a912972d52bd9178d53a0e05c Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 9 Oct 2026 12:01:00 +0200 Subject: [PATCH] feat(editor): the node bank in two panes, as the assets of unity: its groups on the left with the scenery templates, the entries of the chosen one on the right, as cards or a list, a search going through all of them; the models dragged from the node bank onto a model set, the templates of a set or the list of the brush or the area fill; the templates of the model sets shown with their previews; the model sets docked by the node bank by default; Polish translation --- application/editoruilayer.cpp | 8 +- application/editoruipanels.cpp | 475 ++++++++++++++++++++++++--------- application/editoruipanels.h | 16 ++ lang/editor_pl.po | 10 +- 4 files changed, 379 insertions(+), 130 deletions(-) diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index b5d7f462..ee203dda 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -98,8 +98,12 @@ void editor_ui::build_default_layout(unsigned int const Dockspace) ImGui::DockBuilderDockWindow(window, leftbottom); for (auto const *window : {"###inspector", "###editorsettings", "###Include database", "###orthophoto", "###environment"}) ImGui::DockBuilderDockWindow(window, right); - for (auto const *window : {"###Node bank", "###profilestrip", "###trackanalysis", "###structuregauge", "###editorhistory", "###modelsets"}) - ImGui::DockBuilderDockWindow(window, bottom); + // bottom: the node bank, and the model sets by its side, the models are dragged from the one to the other + ImGuiID bottomleft{bottom}; + auto const bottomright{ImGui::DockBuilderSplitNode(bottomleft, ImGuiDir_Right, 0.45f, nullptr, &bottomleft)}; + for (auto const *window : {"###Node bank", "###profilestrip", "###trackanalysis", "###structuregauge", "###editorhistory"}) + ImGui::DockBuilderDockWindow(window, bottomleft); + ImGui::DockBuilderDockWindow("###modelsets", bottomright); ImGui::DockBuilderFinish(Dockspace); } diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index bc157b80..2e177f42 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -59,6 +59,71 @@ bool list_item(char const *Label, bool const Selected) ImGui::PopStyleColor(); return clicked; } + +// name of the payload of a node definition dragged out of the node bank +char const *const template_payload{"EU07_NODEBANK_ENTRY"}; + +// a node definition of the node bank dropped on the item drawn last. returns: true if one was, with its text +bool dropped_template(std::string &Template) +{ + auto dropped{false}; + if (ImGui::BeginDragDropTarget()) + { + if (auto const *payload{ImGui::AcceptDragDropPayload(template_payload)}; payload != nullptr && payload->DataSize > 0) + { + Template.assign(static_cast(payload->Data), static_cast(payload->DataSize)); + dropped = true; + } + ImGui::EndDragDropTarget(); + } + return dropped; +} + +// a card: the image fills its square, or what's missing is said in its place, and the label is under it, cut short with an +// ellipsis. returns: true if it was clicked; the card is the item drawn last +bool card(ImVec2 const &Cell, float const Side, std::uint64_t const Texture, char const *Placeholder, char const *Label, bool const Selected) +{ + auto const &style{ImGui::GetStyle()}; + auto const position{ImGui::GetCursorScreenPos()}; + auto const clicked{ImGui::InvisibleButton("##card", Cell)}; + auto const hovered{ImGui::IsItemHovered()}; + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const end{position.x + Cell.x, position.y + Cell.y}; + list->AddRectFilled(position, end, ImGui::GetColorU32(Selected ? ImGuiCol_Header : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg), style.FrameRounding); + ImVec2 const imagemin{position.x + 2.0f, position.y + 2.0f}; + ImVec2 const imagemax{position.x + Side - 2.0f, position.y + Side - 2.0f}; + if (Texture != 0) + { + list->AddImage(static_cast(Texture), imagemin, imagemax); + } + else + { + list->AddRect(imagemin, imagemax, ImGui::GetColorU32(ImGuiCol_Border), style.FrameRounding); + auto const size{ImGui::CalcTextSize(Placeholder)}; + list->PushClipRect(imagemin, imagemax, true); + list->AddText(ImVec2(imagemin.x + std::max(0.0f, (imagemax.x - imagemin.x - size.x) * 0.5f), imagemin.y + (imagemax.y - imagemin.y - size.y) * 0.5f), ImGui::GetColorU32(ImGuiCol_TextDisabled), Placeholder); + list->PopClipRect(); + } + while (*Label == ' ') + { + ++Label; + } + ImVec2 const labelmin{position.x + style.FramePadding.x, position.y + Side + style.FramePadding.y}; + ImVec2 const labelmax{end.x - style.FramePadding.x, end.y}; + ImGui::RenderTextEllipsis(list, labelmin, labelmax, labelmax.x, labelmax.x, Label, nullptr, nullptr); + return clicked; +} + +// cells of the cards of the side, and how many of them fit the width in a row +ImVec2 card_cell(float const Side) +{ + return ImVec2(Side, Side + ImGui::GetTextLineHeight() + ImGui::GetStyle().FramePadding.y * 2.0f); +} +int card_columns(ImVec2 const &Cell) +{ + auto const &style{ImGui::GetStyle()}; + return std::max(1, static_cast((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (Cell.x + style.ItemSpacing.x))); +} } // namespace void itemproperties_panel::update(scene::basic_node *Node) @@ -602,6 +667,7 @@ void nodebank_panel::nodebank_reload() void nodebank_panel::index_previews() { m_previewpaths.clear(); + m_templatepreviews.clear(); std::error_code error; m_previewfolder = std::filesystem::is_directory(editor_previews::folder, error); // entries with the same model and skin are numbered in the order of the file, the way the generator numbers them @@ -620,12 +686,14 @@ void nodebank_panel::index_previews() auto const name{editor_previews::variant(path, taken)}; taken.insert(name); m_previewpaths[entry.second.get()] = name + ".png"; + m_templatepreviews.try_emplace(*entry.second, name + ".png"); } } void nodebank_panel::preview_popup(std::pair> const &Entry) { - if (false == ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) + // not while something's dragged, the tooltip of the drag is there then + if (false == ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay) || ImGui::GetDragDropPayload() != nullptr) { return; } @@ -669,13 +737,62 @@ void nodebank_panel::preview_popup(std::pairimage(Path)}; + Placeholder = (m_previews->missing(Path) ? STR_C("no preview") : "..."); + return texture; +} + +std::string const &nodebank_panel::template_preview(std::string const &Template) const +{ + static std::string const none; + auto const lookup{m_templatepreviews.find(Template)}; + return lookup != m_templatepreviews.end() ? lookup->second : none; +} + +void nodebank_panel::drag_entry(std::pair> const &Entry) +{ + // the scenery templates are placed with include directives, the lists of the models have no use for them + if (Entry.second->empty() || Entry.second->starts_with(editor_includes::directive_mark)) + { + return; + } + if (false == ImGui::BeginDragDropSource()) + { + return; + } + ImGui::SetDragDropPayload(template_payload, Entry.second->data(), Entry.second->size()); + auto const lookup{m_previewpaths.find(Entry.second.get())}; + if (lookup != m_previewpaths.end() && m_previews != nullptr) + { + if (auto const texture{m_previews->image(lookup->second)}; texture != 0) + { + auto const side{64.0f * std::max(1.0f, Global.ui_scale)}; + ImGui::Image(static_cast(texture), ImVec2(side, side)); + } + } + auto const *label{Entry.first.c_str()}; + while (*label == ' ') + { + ++label; + } + ImGui::TextUnformatted(label); + ImGui::TextDisabled("%s", STR_C("Drop it on a model set, or on the list of the brush or the area fill")); + ImGui::EndDragDropSource(); +} + void nodebank_panel::render_cards(std::vector> const *> const &Entries) { auto const &style{ImGui::GetStyle()}; - auto const card{std::max(16.0f, EditorSettings.nodebank_card() * Global.ui_scale)}; - auto const labelheight{ImGui::GetTextLineHeight()}; - ImVec2 const cell{card, card + labelheight + style.FramePadding.y * 2.0f}; - auto const columns{std::max(1, static_cast((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (cell.x + style.ItemSpacing.x)))}; + auto const side{std::max(16.0f, EditorSettings.nodebank_card() * Global.ui_scale)}; + auto const cell{card_cell(side)}; + auto const columns{card_columns(cell)}; auto const rows{(static_cast(Entries.size()) + columns - 1) / columns}; ImGuiListClipper clipper; clipper.Begin(rows, cell.y + style.ItemSpacing.y); @@ -696,49 +813,15 @@ void nodebank_panel::render_cards(std::vectorsecond : std::string{}, placeholder)}; + if (card(cell, side, texture, placeholder, entry.first.c_str(), entry.second == m_selectedtemplate)) { m_selectedtemplate = entry.second; } - auto const hovered{ImGui::IsItemHovered()}; - auto const selected{entry.second == m_selectedtemplate}; - auto *list{ImGui::GetWindowDrawList()}; - ImVec2 const end{position.x + cell.x, position.y + cell.y}; - list->AddRectFilled(position, end, ImGui::GetColorU32(selected ? ImGuiCol_Header : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg), style.FrameRounding); - // the image fills the square of the card; without one, what's missing is said in its place - ImVec2 const imagemin{position.x + 2.0f, position.y + 2.0f}; - ImVec2 const imagemax{position.x + card - 2.0f, position.y + card - 2.0f}; - auto const lookup{m_previewpaths.find(entry.second.get())}; - std::uint64_t texture{0}; - char const *placeholder{STR_C("not a model")}; - if (lookup != m_previewpaths.end()) - { - texture = m_previews->image(lookup->second); - placeholder = (m_previews->missing(lookup->second) ? STR_C("no preview") : "..."); - } - if (texture != 0) - { - list->AddImage(static_cast(texture), imagemin, imagemax); - } - else - { - list->AddRect(imagemin, imagemax, ImGui::GetColorU32(ImGuiCol_Border), style.FrameRounding); - auto const size{ImGui::CalcTextSize(placeholder)}; - list->PushClipRect(imagemin, imagemax, true); - list->AddText(ImVec2(imagemin.x + std::max(0.0f, (imagemax.x - imagemin.x - size.x) * 0.5f), imagemin.y + (imagemax.y - imagemin.y - size.y) * 0.5f), ImGui::GetColorU32(ImGuiCol_TextDisabled), placeholder); - list->PopClipRect(); - } - // the label under the image, cut short with an ellipsis; the whole of it in the tooltip - auto const *label{entry.first.c_str()}; - while (*label == ' ') - { - ++label; - } - ImVec2 const labelmin{position.x + style.FramePadding.x, position.y + card + style.FramePadding.y}; - ImVec2 const labelmax{end.x - style.FramePadding.x, end.y}; - ImGui::RenderTextEllipsis(list, labelmin, labelmax, labelmax.x, labelmax.x, label, nullptr, nullptr); - if (hovered) + drag_entry(entry); + if (ImGui::IsItemHovered()) { preview_popup(entry); } @@ -749,6 +832,87 @@ void nodebank_panel::render_cards(std::vector> const *> const &Entries) +{ + for (auto const *entry : Entries) + { + ImGui::PushID(entry->second.get()); + auto const label{" " + entry->first}; + if (list_item(label.c_str(), entry->second == m_selectedtemplate)) + { + m_selectedtemplate = entry->second; + } + drag_entry(*entry); + preview_popup(*entry); + ImGui::PopID(); + } +} + +void nodebank_panel::render_templates_list(std::string const &Filter) +{ + if (false == EditorIncludes.scanned()) + { + EditorIncludes.scan(); + } + auto const &templates{EditorIncludes.entries()}; + std::string const *category{nullptr}; + auto listed{0}; + for (auto const entryidx : EditorIncludes.ready()) + { + auto const &entry{templates[entryidx]}; + if (false == Filter.empty() && false == contains(entry.name, Filter) && false == contains(entry.file, Filter) && false == contains(entry.category, Filter)) + { + continue; + } + if (false == entry.category.empty() && (category == nullptr || *category != entry.category)) + { + ImGui::TextDisabled(" %s", entry.category.c_str()); + } + category = &entry.category; + auto const label{(entry.category.empty() ? " " : " ") + entry.name + " (" + Bezogonkow(entry.file) + ")##inc" + std::to_string(entryidx)}; + if (list_item(label.c_str(), entry.statement == m_selectedtemplate)) + m_selectedtemplate = entry.statement; + ++listed; + } + if (listed == 0) + { + ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)"); + } +} + +void nodebank_panel::render_set_cards(int const Setid) +{ + auto const *set{EditorModelSets.find(Setid)}; + if (set == nullptr) + { + return; + } + // a bit smaller than in the node bank, the window of the sets is the smaller one + auto const side{std::max(48.0f, EditorSettings.nodebank_card() * Global.ui_scale * 0.75f)}; + auto const cell{card_cell(side)}; + auto const columns{card_columns(cell)}; + for (int index = 0; index < static_cast(set->templates.size()); ++index) + { + if (index % columns != 0) + { + ImGui::SameLine(); + } + ImGui::PushID(index); + char const *placeholder{nullptr}; + auto const texture{preview_texture(template_preview(set->templates[index]), placeholder)}; + auto const &label{index < static_cast(set->labels.size()) ? set->labels[index] : set->templates[index]}; + if (card(cell, side, texture, placeholder, label.c_str(), index == m_setsentry)) + { + m_setsentry = index; + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", label.c_str()); + } + ImGui::PopID(); + } +} + void nodebank_panel::render() { if (false == is_open) @@ -829,101 +993,132 @@ void nodebank_panel::render() ImGui::TextDisabled("%s", STR_C("No previews yet: they're made by eu07 -generate-nodebank-previews")); } } - if (previews && ImGui::BeginChild("##nodebankcards", ImVec2(-1, listheight), ImGuiChildFlags_Borders)) + // two panes: the groups of the node bank on the left, like folders, and the entries of the chosen one on the right. + // a search goes through all of them + struct category { - // the groups as in the list, the entries of an open one as cards - auto isvisible{false}; - auto const searchfilter{std::string(m_nodesearch)}; - std::vector> const *> cards; - for (auto const &entry : m_nodebank) + std::string const *name; + std::vector> const *> entries; + int matches{0}; + }; + static std::string const ungrouped{"(ungrouped)"}; + auto const searchfilter{std::string(m_nodesearch)}; + std::vector categories; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) { - if (entry.second->empty()) - { - if (false == cards.empty()) - { - render_cards(cards); - cards.clear(); - } - isvisible = ImGui::CollapsingHeader(entry.first.c_str()); - } - else if (isvisible && (searchfilter.empty() || contains(entry.first, searchfilter))) - { - cards.push_back(&entry); - } + categories.push_back({&entry.first, {}}); + continue; } - if (false == cards.empty()) + if (categories.empty()) { - render_cards(cards); + // templates listed before the first header, as group_names() has them + categories.push_back({&ungrouped, {}}); + } + auto &group{categories.back()}; + group.entries.push_back(&entry); + if (false == searchfilter.empty() && contains(entry.first, searchfilter)) + { + ++group.matches; } - ImGui::Separator(); - ImGui::TextDisabled("%s", STR_C("Scenery templates (.inc) are listed with the previews off")); } - if (previews) + auto const templates{Global.editor_session}; + if (m_category >= static_cast(categories.size()) || (m_category == scenery_templates && false == templates) || m_category < scenery_templates) { + m_category = 0; + } + // the panes of the height of the table, inside the padding of its cells + auto const paneheight{listheight - 2.0f * ImGui::GetStyle().CellPadding.y - 1.0f}; + if (ImGui::BeginTable("##nodebankpanes", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV, ImVec2(-1, listheight))) + { + ImGui::TableSetupColumn("##groups", ImGuiTableColumnFlags_WidthFixed, 170.0f * Global.ui_scale); + ImGui::TableSetupColumn("##entries", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (ImGui::BeginChild("##groups", ImVec2(0, paneheight))) + { + auto const searching{false == searchfilter.empty()}; + for (int index = 0; index < static_cast(categories.size()); ++index) + { + auto const &group{categories[index]}; + auto const count{searching ? group.matches : static_cast(group.entries.size())}; + auto const label{*group.name + " (" + std::to_string(count) + ")##group" + std::to_string(index)}; + if (searching && count == 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + } + // a group chosen while searching is shown whole, the search is done + if (list_item(label.c_str(), false == searching && index == m_category)) + { + m_category = index; + m_nodesearch[0] = '\0'; + } + if (searching && count == 0) + { + ImGui::PopStyleColor(); + } + } + if (templates) + { + ImGui::Separator(); + if (list_item(STR_C("Scenery templates (.inc)"), m_category == scenery_templates)) + { + m_category = scenery_templates; + } + } + } ImGui::EndChild(); - } - else if (ImGui::BeginListBox("##nodebank", ImVec2(-1, listheight))) - { - auto idx{0}; - auto isvisible{false}; - auto const searchfilter{std::string(m_nodesearch)}; - for (auto const &entry : m_nodebank) + ImGui::TableSetColumnIndex(1); + if (ImGui::BeginChild("##entries", ImVec2(0, paneheight))) { - if (entry.second->empty()) + if (m_category == scenery_templates) { - // special case, header indicator - isvisible = ImGui::CollapsingHeader(entry.first.c_str()); + render_templates_list(searchfilter); } - else + else if (false == searchfilter.empty()) { - if (false == isvisible) + // what was found, under the names of the groups + std::vector> const *> found; + for (auto const &group : categories) { - continue; + if (group.matches == 0) + { + continue; + } + found.clear(); + for (auto const *entry : group.entries) + { + if (contains(entry->first, searchfilter)) + { + found.push_back(entry); + } + } + ImGui::SeparatorText(group.name->c_str()); + if (previews) + { + render_cards(found); + } + else + { + render_list(found); + } } - if (false == searchfilter.empty() && false == contains(entry.first, searchfilter)) + } + else if (false == categories.empty()) + { + if (previews) { - continue; + render_cards(categories[m_category].entries); + } + else + { + render_list(categories[m_category].entries); } - auto const label{" " + entry.first + "##" + std::to_string(idx)}; - if (list_item(label.c_str(), entry.second == m_selectedtemplate)) - m_selectedtemplate = entry.second; - preview_popup(entry); - ++idx; } } - // scenery templates. placing one adds an include directive to a scenery file, so they take scenery opened for editing - if (Global.editor_session && ImGui::CollapsingHeader(STR_C("Scenery templates (.inc)"))) - { - if (false == EditorIncludes.scanned()) - { - EditorIncludes.scan(); - } - auto const &templates{EditorIncludes.entries()}; - std::string const *category{nullptr}; - auto listed{0}; - for (auto const entryidx : EditorIncludes.ready()) - { - auto const &entry{templates[entryidx]}; - if (false == searchfilter.empty() && false == contains(entry.name, searchfilter) && false == contains(entry.file, searchfilter) && false == contains(entry.category, searchfilter)) - { - continue; - } - if (false == entry.category.empty() && (category == nullptr || *category != entry.category)) - { - ImGui::TextDisabled(" %s", entry.category.c_str()); - } - category = &entry.category; - auto const label{(entry.category.empty() ? " " : " ") + entry.name + " (" + Bezogonkow(entry.file) + ")##inc" + std::to_string(entryidx)}; - if (list_item(label.c_str(), entry.statement == m_selectedtemplate)) - m_selectedtemplate = entry.statement; - ++listed; - } - if (listed == 0) - { - ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)"); - } - } - ImGui::EndListBox(); + ImGui::EndChild(); + ImGui::EndTable(); } ImGui::PopItemWidth(); } @@ -999,6 +1194,11 @@ void nodebank_panel::manual_list(char const *Id, std::vector &List, } ImGui::EndListBox(); } + // a model dragged from the node bank onto the list goes to its end + if (std::string dropped; dropped_template(dropped)) + { + List.push_back(dropped); + } if (ImGui::Button(STR_C("Add selected"))) { if (node_selected()) @@ -1121,7 +1321,8 @@ void nodebank_panel::render_sets_window() // left side: the sets auto const buttonsheight{ImGui::GetFrameHeightWithSpacing()}; ImGui::BeginGroup(); - ImGui::BeginChild("##setlist", ImVec2(190 * Global.ui_scale, -buttonsheight), true); + // the list of the sets takes up to a part of a narrow window, the templates of the set need the room + ImGui::BeginChild("##setlist", ImVec2(std::min(190.0f * Global.ui_scale, ImGui::GetContentRegionAvail().x * 0.4f), -buttonsheight), true); for (auto const &set : sets) { auto const label{set.name + " (" + std::to_string(set.templates.size()) + ")##set" + std::to_string(set.id)}; @@ -1130,6 +1331,13 @@ void nodebank_panel::render_sets_window() m_setsselected = set.id; m_setsentry = -1; } + // a model dragged from the node bank onto a set goes to its end + if (std::string dropped; dropped_template(dropped)) + { + EditorModelSets.add(set.id, {dropped}); + m_setsselected = set.id; + m_setsentry = -1; + } } if (sets.empty()) { @@ -1201,17 +1409,30 @@ void nodebank_panel::render_sets_window() } ImGui::BeginChild("##setentries", ImVec2(0, -buttonsheight * 2), true); - for (int idx = 0; idx < static_cast(edited->labels.size()); ++idx) + // with the previews of the models, as in the node bank + if (EditorSettings.nodebank_previews() && m_previews != nullptr) { - auto const label{edited->labels[idx] + "##entry" + std::to_string(idx)}; - if (list_item(label.c_str(), idx == m_setsentry)) - m_setsentry = idx; + render_set_cards(edited->id); + } + else + { + for (int idx = 0; idx < static_cast(edited->labels.size()); ++idx) + { + auto const label{edited->labels[idx] + "##entry" + std::to_string(idx)}; + if (list_item(label.c_str(), idx == m_setsentry)) + m_setsentry = idx; + } } if (edited->templates.empty()) { ImGui::TextDisabled(STR_C("(empty) select a template in the node bank and press \"Add selected\"")); + ImGui::TextDisabled("%s", STR_C("or drag models here from the node bank")); } ImGui::EndChild(); + if (std::string dropped; dropped_template(dropped)) + { + EditorModelSets.add(edited->id, {dropped}); + } auto const setid{edited->id}; if (ImGui::Button(STR_C("Add selected"))) diff --git a/application/editoruipanels.h b/application/editoruipanels.h index a826d5b4..dbb3f8bc 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -166,6 +166,18 @@ class nodebank_panel : public ui_panel void index_previews(); // entries of a group as cards with their previews, in rows as many as fit the width void render_cards(std::vector> const *> const &Entries); + // the same entries as a list, for when the previews are off + void render_list(std::vector> const *> const &Entries); + // the scenery templates (.inc), as a list; they're placed with include directives, so they take a scenery opened for editing + void render_templates_list(std::string const &Filter); + // the entry dragged from the item drawn last, to drop it on a list of the model sets or of a tool + void drag_entry(std::pair> const &Entry); + // image of a node definition as the node bank has it, or an empty string + std::string const &template_preview(std::string const &Template) const; + // texture of the small preview of the image, 0 if there's none (yet) + std::uint64_t preview_texture(std::string const &Path, char const *&Placeholder); + // the templates of the edited model set as cards with their previews + void render_set_cards(int const Setid); // the entry pointed at for a moment: a popup with its preview, big, and the model and skin it shows void preview_popup(std::pair> const &Entry); // true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take @@ -179,6 +191,10 @@ class nodebank_panel : public ui_panel std::unique_ptr m_previews; std::unique_ptr m_largepreviews; // a few, for the popup bool m_previewfolder{false}; // the generator was run, there's the folder of the images + std::unordered_map m_templatepreviews; // images of the node definitions by their text, for the model sets + // the group of the node bank shown, by its place among the groups; scenery_templates for the scenery templates + static int constexpr scenery_templates{-1}; + int m_category{0}; // set manager window bool m_setsopen{false}; int m_setsselected{0}; // id of the user set being edited diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 1c3dd20c..b043f4e2 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -3860,7 +3860,7 @@ msgid "Not for the node bank yet: %s." msgstr "Jeszcze nie do banku węzłów: %s." msgid "A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\nInsert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\nThe other parameters receive their default values." -msgstr "Szablon wstawiony z banku węzłów dostaje położenie kursora w parametrach pos.*, obrót ustawiony w zakładce\nWstaw w rot.y, a w parametrze name niepowtarzalną nazwę (wartość domyślna z losowym przyrostkiem).\nPozostałe parametry dostają wartości domyślne." +msgstr "Szablon wstawiony z banku obiektów dostaje położenie kursora w parametrach pos.*, obrót ustawiony w zakładce\nWstaw w rot.y, a w parametrze name niepowtarzalną nazwę (wartość domyślna z losowym przyrostkiem).\nPozostałe parametry dostają wartości domyślne." msgid "Suggest roles" msgstr "Podpowiedz role" @@ -5053,3 +5053,11 @@ msgstr "LPM: początek drogi, potem każdy kolejny punkt; na wolnym końcu, boku msgid "Level crossings, and the points where vehicles appear on the roads or are taken off them" msgstr "Przejazdy kolejowe oraz punkty, w których pojazdy pojawiają się na drogach albo są z nich zdejmowane" + +# scenery editor: node bank in two panes, models dragged to the sets + +msgid "Drop it on a model set, or on the list of the brush or the area fill" +msgstr "Upuść go na zestaw modeli albo na listę pędzla lub wypełniania obszaru" + +msgid "or drag models here from the node bank" +msgstr "albo przeciągnij tu modele z banku obiektów"