mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 15:04:54 +02:00
The include bank read every .inc of the scenery directory on the first use, on the main thread: opening the switch panel or selecting a switch stopped the editor until all of them were read, which on a large scenery directory under Windows takes long. The bank now starts looking as the editor starts, in the background, and the main thread only reads the descriptions once the files are read. - the switch drives, turntables and signals are read again from the bank when it changes; the panels show "Looking for the templates..." meanwhile, only the edits which need the templates wait for them - the sweep tool takes the templates from the bank instead of looking through the scenery directory again - a folder linked into one of its own subfolders no longer leads the walk in circles - the log tells how many templates were found and how long it took
2456 lines
78 KiB
C++
2456 lines
78 KiB
C++
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "application/editoruipanels.h"
|
|
#include "scene/scenenodegroups.h"
|
|
#include "scene/scenelayers.h"
|
|
|
|
#include "utilities/Globals.h"
|
|
#include "vehicle/Camera.h"
|
|
#include "model/AnimModel.h"
|
|
#include "world/Track.h"
|
|
#include "world/Event.h"
|
|
#include "world/MemCell.h"
|
|
#include "application/editoruilayer.h"
|
|
#include "rendering/renderer.h"
|
|
#include "simulation/simulation.h"
|
|
#include "editor/editorModelSets.hpp"
|
|
#include "editor/editorFormat.hpp"
|
|
#include "editor/editorSettings.hpp"
|
|
#include "imgui/imgui_internal.h"
|
|
|
|
#include <filesystem>
|
|
#include <set>
|
|
|
|
namespace
|
|
{
|
|
// tells why provided text can't be the name of a node, empty text if it can. the scenery parser has to get it back as a single token
|
|
std::string node_name_issue(std::string const &Name)
|
|
{
|
|
if (Name.find_first_of(" \t\r\n;\"") != std::string::npos)
|
|
{
|
|
return STR_C("a name can't hold spaces, semicolons or quotes");
|
|
}
|
|
if (Name.find("//") != std::string::npos || Name.find("/*") != std::string::npos)
|
|
{
|
|
return STR_C("a name can't hold a comment mark");
|
|
}
|
|
if (Name == "include")
|
|
{
|
|
return STR_C("this word has a meaning of its own in scenery files");
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// list entry; the selected one is drawn with the accent colour, like selections in the starter
|
|
bool list_item(char const *Label, bool const Selected)
|
|
{
|
|
if (Selected)
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImGui::GetStyleColorVec4(ImGuiCol_HeaderActive));
|
|
auto const clicked{ImGui::Selectable(Label, Selected)};
|
|
if (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<char const *>(payload->Data), static_cast<std::size_t>(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<ImTextureID>(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<int>((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (Cell.x + style.ItemSpacing.x)));
|
|
}
|
|
} // namespace
|
|
|
|
void itemproperties_panel::update(scene::basic_node *Node)
|
|
{
|
|
m_node = Node;
|
|
|
|
if (false == is_open)
|
|
{
|
|
return;
|
|
}
|
|
|
|
text_lines.clear();
|
|
m_grouplines.clear();
|
|
|
|
std::string textline;
|
|
|
|
// scenario inspector
|
|
auto const *node{Node};
|
|
auto const &camera{Global.pCamera};
|
|
|
|
if (node == nullptr)
|
|
{
|
|
auto const mouseposition{camera.Pos + GfxRenderer->Mouse_Position()};
|
|
textline = format(STR_C("Cursor: %.2f, %.2f, %.2f"), mouseposition.x, mouseposition.y, mouseposition.z);
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
return;
|
|
}
|
|
/*
|
|
// TODO: bind receiver in the constructor
|
|
if( ( m_itemproperties != nullptr )
|
|
&& ( m_itemproperties->node != nullptr ) ) {
|
|
// fetch node data; skip properties which were changed until they're retrieved by the observer
|
|
auto const *node { m_itemproperties->node };
|
|
|
|
if( m_itemproperties->name.second == false ) {
|
|
m_itemproperties->name.first = ( node->name().empty() ? "(none)" : node->name() );
|
|
}
|
|
if( m_itemproperties->location.second == false ) {
|
|
m_itemproperties->location.first = node->location();
|
|
}
|
|
}
|
|
*/
|
|
textline = format(STR_C("Name: %s"), (node->name().empty() ? std::string{STR_C("(no name)")} : Bezogonkow(node->name())).c_str()) + "\n" + format(STR_C("Type: %s"), node->node_type.c_str());
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
textline = format(STR_C("Location: %.2f, %.2f, %.2f (distance %.1f m)"), node->location().x, node->location().y, node->location().z,
|
|
glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z})) +
|
|
"\nUUID: " + node->uuid.to_string();
|
|
if (scene::Layers.valid(node->layer()))
|
|
{
|
|
textline += "\n" + format(STR_C("Layer: %s"), Bezogonkow(scene::Layers.layer(node->layer()).name).c_str());
|
|
}
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
|
|
// subclass-specific data
|
|
// TBD, TODO: specialized data dump method in each node subclass, or data imports in the panel for provided subclass pointer?
|
|
if (typeid(*node) == typeid(TAnimModel))
|
|
{
|
|
|
|
auto const *subnode = static_cast<TAnimModel const *>(node);
|
|
|
|
std::string lights;
|
|
if (subnode->iNumLights > 0)
|
|
{
|
|
lights += '[';
|
|
for (int lightidx = 0; lightidx < subnode->iNumLights; ++lightidx)
|
|
{
|
|
lights += std::to_string(subnode->lsLights[lightidx]);
|
|
if (lightidx < subnode->iNumLights - 1)
|
|
{
|
|
lights += ", ";
|
|
}
|
|
}
|
|
lights += ']';
|
|
}
|
|
else
|
|
{
|
|
lights = STR_C("(none)");
|
|
}
|
|
textline = format(STR_C("Rotation (x, y, z): %.2f°, %.2f°, %.2f°"), clamp_circular(subnode->vAngle.x, 360.f), clamp_circular(subnode->vAngle.y, 360.f), clamp_circular(subnode->vAngle.z, 360.f)) + "\n" +
|
|
format(STR_C("Lights: %s"), lights.c_str());
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
|
|
// 3d shape
|
|
auto modelfile{(subnode->pModel != nullptr ? subnode->pModel->NameGet() : std::string{STR_C("(none)")})};
|
|
if (modelfile.find(paths::models) == 0)
|
|
{
|
|
// don't include 'models/' in the path
|
|
modelfile.erase(0, std::string{paths::models}.size());
|
|
}
|
|
// texture
|
|
auto texturefile{(subnode->Material()->replacable_skins[1] != null_handle ? GfxRenderer->Material(subnode->Material()->replacable_skins[1])->GetName() : std::string{STR_C("(none)")})};
|
|
if (texturefile.find(paths::textures) == 0)
|
|
{
|
|
// don't include 'textures/' in the path
|
|
texturefile.erase(0, std::string{paths::textures}.size());
|
|
}
|
|
text_lines.emplace_back(format(STR_C("Model: %s"), modelfile.c_str()), Global.UITextColor);
|
|
text_lines.emplace_back(format(STR_C("Texture: %s"), texturefile.c_str()), Global.UITextColor);
|
|
}
|
|
else if (typeid(*node) == typeid(TTrack))
|
|
{
|
|
|
|
auto const *subnode = static_cast<TTrack const *>(node);
|
|
|
|
std::string isolatedlist;
|
|
for (const TIsolated *iso : subnode->Isolated)
|
|
{
|
|
if (!isolatedlist.empty())
|
|
isolatedlist += ", ";
|
|
isolatedlist += iso->asName;
|
|
}
|
|
|
|
// basic attributes
|
|
textline = format(STR_C("Track circuits: %s"), (!isolatedlist.empty() ? isolatedlist : std::string{STR_C("(none)")}).c_str()) + "\n" +
|
|
format(STR_C("Speed limit: %.0f km/h"), static_cast<double>(subnode->SwitchExtension ? subnode->SwitchExtension->fVelocity : subnode->fVelocity)) + "\n" +
|
|
format(STR_C("Track gauge: %.0f mm"), subnode->fTrackWidth * 1000.0) + "\n" + format(STR_C("Friction: %.2f"), static_cast<double>(subnode->fFriction)) + "\n" +
|
|
format(STR_C("Quality: %d"), static_cast<int>(subnode->iQualityFlag));
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
// textures
|
|
auto texturefile{(subnode->m_material1 != null_handle ? GfxRenderer->Material(subnode->m_material1)->GetName() : std::string{STR_C("(none)")})};
|
|
if (texturefile.find(paths::textures) == 0)
|
|
{
|
|
texturefile.erase(0, std::string{paths::textures}.size());
|
|
}
|
|
auto texturefile2{(subnode->m_material2 != null_handle ? GfxRenderer->Material(subnode->m_material2)->GetName() : std::string{STR_C("(none)")})};
|
|
if (texturefile2.find(paths::textures) == 0)
|
|
{
|
|
texturefile2.erase(0, std::string{paths::textures}.size());
|
|
}
|
|
textline = format(STR_C("Textures: %s, %s"), texturefile.c_str(), texturefile2.c_str());
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
// paths
|
|
textline = STR_C("Geometry:");
|
|
for (auto const &path : subnode->m_paths)
|
|
{
|
|
textline += "\n [" + to_string(path.points[segment_data::point::start].x, 3) + ", " + to_string(path.points[segment_data::point::start].y, 3) + ", " +
|
|
to_string(path.points[segment_data::point::start].z, 3) + "]->" + " [" + to_string(path.points[segment_data::point::end].x, 3) + ", " +
|
|
to_string(path.points[segment_data::point::end].y, 3) + ", " + to_string(path.points[segment_data::point::end].z, 3) + "] ";
|
|
}
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
// events
|
|
textline.clear();
|
|
|
|
std::vector<std::pair<std::string, TTrack::event_sequence const *>> const eventsequences{{"ev0", &subnode->m_events0}, {"ev0all", &subnode->m_events0all},
|
|
{"ev1", &subnode->m_events1}, {"ev1all", &subnode->m_events1all},
|
|
{"ev2", &subnode->m_events2}, {"ev2all", &subnode->m_events2all}};
|
|
|
|
for (auto const &eventsequence : eventsequences)
|
|
{
|
|
|
|
if (eventsequence.second->empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
textline += (textline.empty() ? "" : "\n") + eventsequence.first + ": [";
|
|
for (auto const &event : *eventsequence.second)
|
|
{
|
|
if (textline.back() != '[')
|
|
{
|
|
textline += ", ";
|
|
}
|
|
textline += event.second != nullptr ? Bezogonkow(event.second->m_name) : event.first + STR_C(" (missing)");
|
|
}
|
|
textline += "] ";
|
|
}
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
}
|
|
else if (typeid(*node) == typeid(TMemCell))
|
|
{
|
|
|
|
auto const *subnode = static_cast<TMemCell const *>(node);
|
|
|
|
textline = format(STR_C("Data: [%s] [%.2f] [%.2f]"), subnode->Text().c_str(), subnode->Value1(), subnode->Value2());
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
textline = format(STR_C("Track: %s"), (subnode->asTrackName.empty() ? std::string{STR_C("(none)")} : Bezogonkow(subnode->asTrackName)).c_str());
|
|
text_lines.emplace_back(textline, Global.UITextColor);
|
|
}
|
|
|
|
update_group();
|
|
}
|
|
|
|
void itemproperties_panel::update_group()
|
|
{
|
|
|
|
auto const grouphandle{m_node->group()};
|
|
|
|
if (grouphandle == null_handle)
|
|
{
|
|
m_grouphandle = null_handle;
|
|
m_groupprefix.clear();
|
|
return;
|
|
}
|
|
|
|
auto const &nodegroup{scene::Groups.group(grouphandle)};
|
|
|
|
if (m_grouphandle != grouphandle)
|
|
{
|
|
// calculate group name from shared prefix of item names
|
|
std::vector<std::reference_wrapper<std::string const>> names;
|
|
// build list of custom item and event names
|
|
for (auto const *node : nodegroup.nodes)
|
|
{
|
|
auto const &name{node->name()};
|
|
if (name.empty() || name == "none")
|
|
{
|
|
continue;
|
|
}
|
|
names.emplace_back(name);
|
|
}
|
|
for (auto const *event : nodegroup.events)
|
|
{
|
|
auto const &name{event->m_name};
|
|
if (name.empty() || name == "none")
|
|
{
|
|
continue;
|
|
}
|
|
names.emplace_back(name);
|
|
}
|
|
// find the common prefix
|
|
if (names.size() > 1)
|
|
{
|
|
m_groupprefix = names.front();
|
|
for (auto const &name : names)
|
|
{
|
|
// NOTE: first calculation runs over two instances of the same name, but, eh
|
|
auto const prefixlength{len_common_prefix(m_groupprefix, name.get())};
|
|
if (prefixlength > 0)
|
|
{
|
|
m_groupprefix = m_groupprefix.substr(0, prefixlength);
|
|
}
|
|
else
|
|
{
|
|
m_groupprefix.clear();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// less than two names to compare means no prefix
|
|
m_groupprefix.clear();
|
|
}
|
|
m_grouphandle = grouphandle;
|
|
}
|
|
|
|
m_grouplines.emplace_back(format(STR_C("Objects: %zu, events: %zu"), nodegroup.nodes.size(), nodegroup.events.size()), Global.UITextColor);
|
|
m_grouplines.emplace_back(format(STR_C("Common start of the names: %s"), (m_groupprefix.empty() ? std::string{STR_C("(none)")} : m_groupprefix).c_str()), Global.UITextColor);
|
|
}
|
|
|
|
void itemproperties_panel::render()
|
|
{
|
|
|
|
if (false == is_open)
|
|
{
|
|
return;
|
|
}
|
|
if (true == text_lines.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
|
|
|
|
if (size.x > 0)
|
|
{
|
|
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
|
|
}
|
|
if (size_min.x > 0)
|
|
{
|
|
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2(size_max.x, size_max.y));
|
|
}
|
|
auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name};
|
|
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
|
|
{
|
|
render_body();
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void itemproperties_panel::render_body()
|
|
{
|
|
if (m_include != nullptr)
|
|
{
|
|
render_include();
|
|
return;
|
|
}
|
|
// header section
|
|
auto const model{m_node != nullptr && typeid(*m_node) == typeid(TAnimModel)};
|
|
auto details{text_lines.begin()};
|
|
if (false == model && details != text_lines.end())
|
|
{
|
|
ImGui::TextColored(ImVec4(details->color.r, details->color.g, details->color.b, details->color.a), "%s", details->data.c_str());
|
|
++details;
|
|
}
|
|
// name of the node — TAnimModel only
|
|
render_name_editor();
|
|
// transform editor (position/rotation/scale) — TAnimModel only
|
|
render_transform_editor();
|
|
// group section
|
|
render_group();
|
|
ImGui::PushID("nodedetails");
|
|
if (details != text_lines.end() && ImGui::TreeNode(STR_C("More settings")))
|
|
{
|
|
for (; details != text_lines.end(); ++details)
|
|
{
|
|
ImGui::TextColored(ImVec4(details->color.r, details->color.g, details->color.b, details->color.a), "%s", details->data.c_str());
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
// Name of a picked TAnimModel. The editor places decorations without names, as each name takes its share of the time
|
|
// the scenery needs to load; this is where an instance gets one, for the events to refer to it.
|
|
void itemproperties_panel::render_name_editor()
|
|
{
|
|
if (m_node == nullptr) { return; }
|
|
if (typeid(*m_node) != typeid(TAnimModel)) { return; }
|
|
auto *picked = static_cast<TAnimModel *>(m_node);
|
|
// the name of an instance defined by a template, or by a file which can't be rewritten, wouldn't make it to the scenery files
|
|
if (false == scene::Layers.editable(picked)) { return; }
|
|
|
|
if (m_namednode != m_node || false == m_nameactive)
|
|
{
|
|
// the field shows the name of the node unless it's being typed in
|
|
if (m_namednode != m_node)
|
|
{
|
|
m_nameissue.clear();
|
|
}
|
|
m_namednode = m_node;
|
|
std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str());
|
|
}
|
|
auto const entered{ImGui::InputTextWithHint(STR_C("name"), STR_C("(none)"), m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)};
|
|
m_nameactive = ImGui::IsItemActive();
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it"));
|
|
}
|
|
if (entered || ImGui::IsItemDeactivatedAfterEdit())
|
|
{
|
|
// names are lower case, like everything else the scenery parser hands over
|
|
std::string name{m_namebuffer};
|
|
for (auto &character : name)
|
|
{
|
|
if (character >= 'A' && character <= 'Z')
|
|
{
|
|
character = static_cast<char>(character - 'A' + 'a');
|
|
}
|
|
}
|
|
if (name == "none")
|
|
{
|
|
name.clear();
|
|
}
|
|
if (name != picked->name())
|
|
{
|
|
m_nameissue = node_name_issue(name);
|
|
if (m_nameissue.empty() && false == simulation::State.rename_model(picked, name))
|
|
{
|
|
m_nameissue = STR_C("another model instance goes by this name");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_nameissue.clear();
|
|
}
|
|
m_nameactive = false;
|
|
}
|
|
if (false == m_nameissue.empty())
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", m_nameissue.c_str());
|
|
}
|
|
}
|
|
|
|
// In-place editor for position (double precision), rotation (degrees, 0-360),
|
|
// and uniform scale (per-axis float, 1.000) of a picked TAnimModel.
|
|
// Other node subclasses don't expose these knobs through the same API, so the
|
|
// editor short-circuits when the bound node isn't a TAnimModel.
|
|
void itemproperties_panel::render_transform_editor()
|
|
{
|
|
if (m_node == nullptr) { return; }
|
|
if (typeid(*m_node) != typeid(TAnimModel)) { return; }
|
|
auto *picked = static_cast<TAnimModel *>(m_node);
|
|
|
|
if (false == ImGui::CollapsingHeader(STR_C("Transform"), ImGuiTreeNodeFlags_DefaultOpen))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Position — full double precision via DragScalarN. World coordinates can grow
|
|
// large; %.6f gives sub-millimetre resolution at typical scenario distances.
|
|
{
|
|
glm::dvec3 location = picked->location();
|
|
double pos[3] = { location.x, location.y, location.z };
|
|
if (ImGui::DragScalarN(STR_C("position (m)"), ImGuiDataType_Double, pos, 3, 0.05f, nullptr, nullptr, "%.6f"))
|
|
{
|
|
picked->location(glm::dvec3(pos[0], pos[1], pos[2]));
|
|
}
|
|
}
|
|
|
|
// Rotation — wrapped into [0,360) for display; slider clamps drags to that range.
|
|
{
|
|
glm::vec3 angles{
|
|
clamp_circular(picked->Angles().x),
|
|
clamp_circular(picked->Angles().y),
|
|
clamp_circular(picked->Angles().z)};
|
|
if (ImGui::DragFloat3(STR_C("rotation (deg)"), &angles.x, 0.5f, 0.0f, 360.0f, "%.3f"))
|
|
{
|
|
picked->Angles(angles);
|
|
}
|
|
}
|
|
|
|
// Scale — per-axis float, 1.000 display. Clamped to a reasonable positive range.
|
|
{
|
|
glm::vec3 scale = picked->Scale();
|
|
if (ImGui::DragFloat3(STR_C("scale (x,y,z)"), &scale.x, 0.01f, 0.001f, 100.0f, "%.3f"))
|
|
{
|
|
picked->Scale(scale);
|
|
}
|
|
}
|
|
|
|
if (ImGui::Button(STR_C("reset rotation"))) { picked->Angles(glm::vec3(0.0f)); }
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("reset scale"))) { picked->Scale(glm::vec3(1.0f)); }
|
|
}
|
|
|
|
bool itemproperties_panel::render_group()
|
|
{
|
|
|
|
if (m_node == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
if (m_grouplines.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (false == ImGui::CollapsingHeader(STR_C("Parent Group")))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (auto const &line : m_grouplines)
|
|
{
|
|
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), "%s", line.data.c_str());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string *brush_object_list::GetRandomObject()
|
|
{
|
|
static std::string empty; // fallback
|
|
|
|
if (Objects.empty())
|
|
return ∅
|
|
|
|
std::uniform_int_distribution<size_t> dist(0, Objects.size() - 1);
|
|
|
|
return &Objects[dist(Global.local_random_engine)];
|
|
}
|
|
|
|
void brush_object_list::render_options(nodebank_panel &Bank)
|
|
{
|
|
ImGui::SliderFloat(STR_C("Spacing"), &spacing, 0.1f, 20.0f, "%.1f m");
|
|
ImGui::Checkbox(STR_C("Random model from set"), &useRandom);
|
|
if (false == useRandom)
|
|
{
|
|
ImGui::TextDisabled(STR_C("Paints the template selected in the node bank"));
|
|
return;
|
|
}
|
|
Bank.set_combo(STR_C("Preset"), source, STR_C("Manual list"));
|
|
if (source.kind == model_set_ref::source::manual)
|
|
{
|
|
Bank.manual_list("brushset", Objects, idx);
|
|
}
|
|
else
|
|
{
|
|
auto const count{Bank.set_entries(source).size()};
|
|
ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? STR_C(", the node bank selection is used") : "");
|
|
}
|
|
}
|
|
|
|
nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen)
|
|
{
|
|
size_min = {100, 50};
|
|
size_max = {1000, 1000};
|
|
|
|
memset(m_nodesearch, 0, sizeof(m_nodesearch));
|
|
|
|
std::ifstream file;
|
|
file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary);
|
|
|
|
std::string line;
|
|
while (std::getline(file, line))
|
|
{
|
|
if (line.size() < 4)
|
|
{
|
|
continue;
|
|
}
|
|
auto const labelend{line.find("node")};
|
|
auto const nodedata{(labelend == std::string::npos ? "" : labelend == 0 ? line : line.substr(labelend))};
|
|
auto const label{(labelend == std::string::npos ? line : labelend == 0 ? generate_node_label(nodedata) : line.substr(0, labelend))};
|
|
|
|
m_nodebank.push_back({label, std::make_shared<std::string>(nodedata)});
|
|
}
|
|
index_previews();
|
|
// sort alphabetically content of each group
|
|
auto groupbegin{m_nodebank.begin()};
|
|
auto groupend{groupbegin};
|
|
while (groupbegin != m_nodebank.end())
|
|
{
|
|
groupbegin = std::find_if(groupend, m_nodebank.end(), [](auto const &Entry) { return false == Entry.second->empty(); });
|
|
groupend = std::find_if(groupbegin, m_nodebank.end(), [](auto const &Entry) { return Entry.second->empty(); });
|
|
std::sort(groupbegin, groupend, [](auto const &Left, auto const &Right) { return Left.first < Right.first; });
|
|
}
|
|
}
|
|
void nodebank_panel::nodebank_reload()
|
|
{
|
|
m_nodebank.clear();
|
|
// images made since are read anew
|
|
if (m_previews)
|
|
{
|
|
m_previews->clear();
|
|
}
|
|
if (m_largepreviews)
|
|
{
|
|
m_largepreviews->clear();
|
|
}
|
|
std::ifstream file;
|
|
file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary);
|
|
std::string line;
|
|
while (std::getline(file, line))
|
|
{
|
|
if (line.size() < 4)
|
|
{
|
|
continue;
|
|
}
|
|
auto const labelend{line.find("node")};
|
|
auto const nodedata{(labelend == std::string::npos ? "" : labelend == 0 ? line : line.substr(labelend))};
|
|
auto const label{(labelend == std::string::npos ? line : labelend == 0 ? generate_node_label(nodedata) : line.substr(0, labelend))};
|
|
|
|
m_nodebank.push_back({label, std::make_shared<std::string>(nodedata)});
|
|
}
|
|
index_previews();
|
|
// sort alphabetically content of each group
|
|
auto groupbegin{m_nodebank.begin()};
|
|
auto groupend{groupbegin};
|
|
while (groupbegin != m_nodebank.end())
|
|
{
|
|
groupbegin = std::find_if(groupend, m_nodebank.end(), [](auto const &Entry) { return false == Entry.second->empty(); });
|
|
groupend = std::find_if(groupbegin, m_nodebank.end(), [](auto const &Entry) { return Entry.second->empty(); });
|
|
std::sort(groupbegin, groupend, [](auto const &Left, auto const &Right) { return Left.first < Right.first; });
|
|
}
|
|
}
|
|
|
|
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
|
|
std::set<std::string> taken;
|
|
for (auto const &entry : m_nodebank)
|
|
{
|
|
if (entry.second->empty())
|
|
{
|
|
continue;
|
|
}
|
|
auto const path{editor_previews::entry_preview_path(*entry.second)};
|
|
if (path.empty())
|
|
{
|
|
continue;
|
|
}
|
|
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<std::string, std::shared_ptr<std::string>> const &Entry)
|
|
{
|
|
// 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;
|
|
}
|
|
// the generator makes the images 1024 pixels big; they're shown half as big, as much as the window has room for
|
|
auto const size{std::clamp(static_cast<int>(512.0f * Global.ui_scale), 256, 1024)};
|
|
if (m_largepreviews == nullptr)
|
|
{
|
|
m_largepreviews = std::make_unique<editor_previews::image_cache>(size, 16);
|
|
}
|
|
auto const *label{Entry.first.c_str()};
|
|
while (*label == ' ')
|
|
{
|
|
++label;
|
|
}
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextUnformatted(label);
|
|
std::string model, skin;
|
|
if (editor_previews::entry_model(*Entry.second, model, skin))
|
|
{
|
|
ImGui::TextDisabled("%s%s%s", model.c_str(), (skin.empty() || skin == "none") ? "" : " ", (skin.empty() || skin == "none") ? "" : skin.c_str());
|
|
}
|
|
auto const lookup{m_previewpaths.find(Entry.second.get())};
|
|
if (lookup != m_previewpaths.end())
|
|
{
|
|
auto const side{std::min(static_cast<float>(size), ImGui::GetMainViewport()->WorkSize.y * 0.6f)};
|
|
auto texture{m_largepreviews->image(lookup->second)};
|
|
if (texture == 0 && m_previews)
|
|
{
|
|
// the small image, until the big one is read
|
|
texture = m_previews->image(lookup->second);
|
|
}
|
|
if (texture != 0)
|
|
{
|
|
ImGui::Image(static_cast<ImTextureID>(texture), ImVec2(side, side));
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextDisabled("%s", m_largepreviews->missing(lookup->second) ? STR_C("no preview") : "...");
|
|
}
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
std::uint64_t nodebank_panel::preview_texture(std::string const &Path, char const *&Placeholder)
|
|
{
|
|
if (Path.empty() || m_previews == nullptr)
|
|
{
|
|
Placeholder = STR_C("not a model");
|
|
return 0;
|
|
}
|
|
auto const texture{m_previews->image(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<std::string, std::shared_ptr<std::string>> 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<ImTextureID>(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<std::pair<std::string, std::shared_ptr<std::string>> const *> const &Entries)
|
|
{
|
|
auto const &style{ImGui::GetStyle()};
|
|
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<int>(Entries.size()) + columns - 1) / columns};
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(rows, cell.y + style.ItemSpacing.y);
|
|
while (clipper.Step())
|
|
{
|
|
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
|
{
|
|
for (int column = 0; column < columns; ++column)
|
|
{
|
|
auto const index{static_cast<std::size_t>(row * columns + column)};
|
|
if (index >= Entries.size())
|
|
{
|
|
break;
|
|
}
|
|
auto const &entry{*Entries[index]};
|
|
if (column > 0)
|
|
{
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::PushID(entry.second.get());
|
|
auto const lookup{m_previewpaths.find(entry.second.get())};
|
|
char const *placeholder{nullptr};
|
|
auto const texture{preview_texture(lookup != m_previewpaths.end() ? lookup->second : std::string{}, placeholder)};
|
|
if (card(cell, side, texture, placeholder, entry.first.c_str(), entry.second == m_selectedtemplate))
|
|
{
|
|
m_selectedtemplate = entry.second;
|
|
}
|
|
drag_entry(entry);
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
preview_popup(entry);
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
}
|
|
clipper.End();
|
|
}
|
|
|
|
void nodebank_panel::render_list(std::vector<std::pair<std::string, std::shared_ptr<std::string>> 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)
|
|
{
|
|
EditorIncludes.start();
|
|
if (EditorIncludes.searching() && EditorIncludes.ready().empty())
|
|
{
|
|
ImGui::TextDisabled("%s", STR_C("Looking for the templates..."));
|
|
return;
|
|
}
|
|
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("%s", EditorIncludes.ready().empty() ? STR_C(" (none yet, describe the templates in the Include database window)") : STR_C(" (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<int>(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<int>(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 || suppressed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
|
|
|
|
if (size.x > 0)
|
|
{
|
|
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
|
|
}
|
|
else
|
|
{
|
|
ImGui::SetNextWindowSize(ImVec2S(440, 400), ImGuiCond_FirstUseEver);
|
|
}
|
|
if (size_min.x > 0)
|
|
{
|
|
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y));
|
|
}
|
|
if (m_standin)
|
|
{
|
|
// the same window, so it keeps its place in the dock
|
|
if (ImGui::Begin((m_standintitle + "###" + name()).c_str(), nullptr, flags))
|
|
m_standin();
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + name()};
|
|
|
|
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
|
|
{
|
|
if (m_largepreviews)
|
|
{
|
|
m_largepreviews->update();
|
|
}
|
|
if (ImGui::Button(m_setsopen ? STR_C("Close model sets") : STR_C("Model sets...")))
|
|
{
|
|
m_setsopen = !m_setsopen;
|
|
}
|
|
// cards with the previews of the models, or the list
|
|
ImGui::SameLine();
|
|
auto previews{EditorSettings.nodebank_previews()};
|
|
if (ImGui::Checkbox(STR_C("Previews"), &previews))
|
|
{
|
|
EditorSettings.nodebank_previews(previews);
|
|
EditorSettings.save();
|
|
}
|
|
if (ImGui::TreeNode(STR_C("More settings")))
|
|
{
|
|
if (ImGui::Button(STR_C("Reload node bank")))
|
|
{
|
|
nodebank_reload();
|
|
}
|
|
if (previews)
|
|
{
|
|
ImGui::SameLine();
|
|
auto card{EditorSettings.nodebank_card()};
|
|
ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 120.0f * Global.ui_scale));
|
|
if (ImGui::SliderInt("##cardsize", &card, 48, 256, STR_C("%d px")))
|
|
{
|
|
EditorSettings.nodebank_card(card);
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
{
|
|
EditorSettings.save();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("%s", STR_C("Size of the cards with the previews"));
|
|
}
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
// the edit modes are chosen in the toolbar and set up in the tool options window, the gizmo and the node
|
|
// properties are in the inspector: what's left here is the node bank itself
|
|
ImGui::Separator();
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputTextWithHint(STR_C("Search"), STR_C("Search node bank"), m_nodesearch, IM_ARRAYSIZE(m_nodesearch));
|
|
// the list takes the rest of the window, but keeps a usable height when the sections above are expanded (the window scrolls then)
|
|
auto const listheight{std::max(ImGui::GetContentRegionAvail().y, ImGui::GetTextLineHeightWithSpacing() * 10.0f)};
|
|
if (previews)
|
|
{
|
|
if (m_previews == nullptr)
|
|
{
|
|
m_previews = std::make_unique<editor_previews::image_cache>();
|
|
}
|
|
m_previews->update();
|
|
if (false == m_previewfolder)
|
|
{
|
|
ImGui::TextDisabled("%s", STR_C("No previews yet: they're made by eu07 -generate-nodebank-previews"));
|
|
}
|
|
}
|
|
// 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
|
|
{
|
|
std::string const *name;
|
|
std::vector<std::pair<std::string, std::shared_ptr<std::string>> const *> entries;
|
|
int matches{0};
|
|
};
|
|
std::string const ungrouped{STR_C("(ungrouped)")};
|
|
auto const searchfilter{std::string(m_nodesearch)};
|
|
std::vector<category> categories;
|
|
for (auto const &entry : m_nodebank)
|
|
{
|
|
if (entry.second->empty())
|
|
{
|
|
categories.push_back({&entry.first, {}});
|
|
continue;
|
|
}
|
|
if (categories.empty())
|
|
{
|
|
// 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;
|
|
}
|
|
}
|
|
auto const templates{Global.editor_session};
|
|
if (m_category >= static_cast<int>(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<int>(categories.size()); ++index)
|
|
{
|
|
auto const &group{categories[index]};
|
|
auto const count{searching ? group.matches : static_cast<int>(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();
|
|
ImGui::TableSetColumnIndex(1);
|
|
if (ImGui::BeginChild("##entries", ImVec2(0, paneheight)))
|
|
{
|
|
if (m_category == scenery_templates)
|
|
{
|
|
render_templates_list(searchfilter);
|
|
}
|
|
else if (false == searchfilter.empty())
|
|
{
|
|
// what was found, under the names of the groups
|
|
std::vector<std::pair<std::string, std::shared_ptr<std::string>> const *> found;
|
|
for (auto const &group : categories)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
else if (false == categories.empty())
|
|
{
|
|
if (previews)
|
|
{
|
|
render_cards(categories[m_category].entries);
|
|
}
|
|
else
|
|
{
|
|
render_list(categories[m_category].entries);
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
}
|
|
|
|
ImGui::End();
|
|
|
|
if (m_setsopen)
|
|
{
|
|
render_sets_window();
|
|
}
|
|
}
|
|
|
|
bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const *Manuallabel)
|
|
{
|
|
auto changed{false};
|
|
auto const preview{set_name(Ref, Manuallabel)};
|
|
if (false == ImGui::BeginCombo(Label, preview.c_str()))
|
|
{
|
|
return false;
|
|
}
|
|
if (Manuallabel != nullptr)
|
|
{
|
|
if (ImGui::Selectable(Manuallabel, Ref.kind == model_set_ref::source::manual))
|
|
{
|
|
Ref = model_set_ref{};
|
|
changed = true;
|
|
}
|
|
}
|
|
ImGui::TextDisabled(STR_C("User sets"));
|
|
auto const &sets{EditorModelSets.sets()};
|
|
if (sets.empty())
|
|
{
|
|
ImGui::TextDisabled(STR_C(" (none yet, see Model sets...)"));
|
|
}
|
|
for (auto const &set : sets)
|
|
{
|
|
auto const label{" " + set.name + " (" + std::to_string(set.templates.size()) + ")##userset" + std::to_string(set.id)};
|
|
if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::user && Ref.id == set.id))
|
|
{
|
|
Ref = {model_set_ref::source::user, set.id};
|
|
changed = true;
|
|
}
|
|
}
|
|
ImGui::TextDisabled(STR_C("Node bank groups"));
|
|
auto const groups{group_names()};
|
|
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
|
|
{
|
|
auto const label{" " + groups[idx] + "##bankgroup" + std::to_string(idx)};
|
|
if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::nodebank && Ref.id == idx))
|
|
{
|
|
Ref = {model_set_ref::source::nodebank, idx};
|
|
changed = true;
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
return changed;
|
|
}
|
|
|
|
void nodebank_panel::manual_list(char const *Id, std::vector<std::string> &List, int &Selected)
|
|
{
|
|
ImGui::PushID(Id);
|
|
if (ImGui::BeginListBox("##list", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 6.5f)))
|
|
{
|
|
for (int idx = 0; idx < static_cast<int>(List.size()); ++idx)
|
|
{
|
|
auto const label{generate_node_label(List[idx]) + "##" + std::to_string(idx)};
|
|
if (list_item(label.c_str(), Selected == idx))
|
|
Selected = idx;
|
|
}
|
|
if (List.empty())
|
|
{
|
|
ImGui::TextDisabled(STR_C("(empty)"));
|
|
}
|
|
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())
|
|
List.push_back(*m_selectedtemplate);
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Remove")) && Selected >= 0 && Selected < static_cast<int>(List.size()))
|
|
{
|
|
List.erase(List.begin() + Selected);
|
|
Selected = -1;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Clear")))
|
|
{
|
|
List.clear();
|
|
Selected = -1;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Save as set")) && false == List.empty())
|
|
{
|
|
open_sets_window(EditorModelSets.create(STR_C("New set"), List));
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
std::vector<std::string const *> nodebank_panel::set_entries(model_set_ref const &Ref) const
|
|
{
|
|
std::vector<std::string const *> entries;
|
|
if (Ref.kind == model_set_ref::source::user)
|
|
{
|
|
if (auto const *set = EditorModelSets.find(Ref.id))
|
|
{
|
|
for (auto const &entry : set->templates)
|
|
entries.push_back(&entry);
|
|
}
|
|
}
|
|
else if (Ref.kind == model_set_ref::source::nodebank && Ref.id >= 0)
|
|
{
|
|
// same grouping as group_templates(), without copying the templates
|
|
int group{0};
|
|
bool started{false};
|
|
for (auto const &entry : m_nodebank)
|
|
{
|
|
if (entry.second->empty())
|
|
{
|
|
if (started)
|
|
++group;
|
|
started = true;
|
|
continue;
|
|
}
|
|
started = true;
|
|
if (group == Ref.id)
|
|
entries.push_back(entry.second.get());
|
|
else if (group > Ref.id)
|
|
break;
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
std::string const *nodebank_panel::random_template(model_set_ref const &Ref) const
|
|
{
|
|
auto const entries{set_entries(Ref)};
|
|
if (entries.empty())
|
|
return nullptr;
|
|
|
|
std::uniform_int_distribution<std::size_t> dist(0, entries.size() - 1);
|
|
return entries[dist(Global.local_random_engine)];
|
|
}
|
|
|
|
std::string nodebank_panel::set_name(model_set_ref const &Ref, char const *Manuallabel) const
|
|
{
|
|
switch (Ref.kind)
|
|
{
|
|
case model_set_ref::source::user:
|
|
{
|
|
auto const *set = EditorModelSets.find(Ref.id);
|
|
return set != nullptr ? set->name : std::string{STR_C("(deleted set)")};
|
|
}
|
|
case model_set_ref::source::nodebank:
|
|
{
|
|
auto const groups{group_names()};
|
|
return Ref.id >= 0 && Ref.id < static_cast<int>(groups.size()) ? format(STR_C("Node bank: %s"), groups[Ref.id].c_str()) : std::string{STR_C("(missing node bank group)")};
|
|
}
|
|
default:
|
|
return Manuallabel != nullptr ? Manuallabel : STR_C("(none)");
|
|
}
|
|
}
|
|
|
|
void nodebank_panel::open_sets_window(int const Setid)
|
|
{
|
|
m_setsopen = true;
|
|
if (Setid != 0)
|
|
{
|
|
m_setsselected = Setid;
|
|
m_setsentry = -1;
|
|
}
|
|
}
|
|
|
|
void nodebank_panel::render_sets_window()
|
|
{
|
|
ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver);
|
|
if (false == ImGui::Begin((std::string(STR_C("Model sets")) + "###modelsets").c_str(), &m_setsopen, ImGuiWindowFlags_NoCollapse))
|
|
{
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
auto const &sets{EditorModelSets.sets()};
|
|
if (EditorModelSets.find(m_setsselected) == nullptr)
|
|
{
|
|
m_setsselected = sets.empty() ? 0 : sets.front().id;
|
|
m_setsentry = -1;
|
|
}
|
|
|
|
// left side: the sets
|
|
auto const buttonsheight{ImGui::GetFrameHeightWithSpacing()};
|
|
ImGui::BeginGroup();
|
|
// 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)};
|
|
if (list_item(label.c_str(), set.id == m_setsselected))
|
|
{
|
|
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())
|
|
{
|
|
ImGui::TextDisabled(STR_C("No sets yet"));
|
|
}
|
|
ImGui::EndChild();
|
|
if (ImGui::Button(STR_C("New")))
|
|
{
|
|
open_sets_window(EditorModelSets.create(STR_C("New set")));
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("%s", STR_C("Makes an empty set. \"Save as set\" on a brush or area fill list makes one too.\nThe sets are chosen in Insert (random model), Brush and Area fill"));
|
|
}
|
|
auto const *edited{EditorModelSets.find(m_setsselected)};
|
|
if (edited != nullptr)
|
|
{
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Duplicate")))
|
|
{
|
|
open_sets_window(EditorModelSets.create(format(STR_C("%s copy"), edited->name.c_str()), edited->templates));
|
|
edited = EditorModelSets.find(m_setsselected);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Delete")))
|
|
{
|
|
ImGui::OpenPopup("##deleteset");
|
|
}
|
|
if (ImGui::BeginPopup("##deleteset"))
|
|
{
|
|
ImGui::Text(STR_C("Delete set \"%s\"?"), edited->name.c_str());
|
|
if (ImGui::Button(STR_C("Delete##confirm")))
|
|
{
|
|
EditorModelSets.remove(edited->id);
|
|
edited = nullptr;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Cancel")))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::SameLine();
|
|
|
|
// right side: content of the selected set
|
|
ImGui::BeginGroup();
|
|
if (edited != nullptr)
|
|
{
|
|
if (m_setsnameid != edited->id)
|
|
{
|
|
// selection changed, load its name into the edit buffer
|
|
std::snprintf(m_setsname, sizeof(m_setsname), "%s", edited->name.c_str());
|
|
m_setsnameid = edited->id;
|
|
}
|
|
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Rename")).x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
|
|
auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)};
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Rename")) || entered)
|
|
{
|
|
EditorModelSets.rename(edited->id, m_setsname);
|
|
m_setsnameid = 0; // reload, the name could have been adjusted to stay unique
|
|
}
|
|
|
|
ImGui::BeginChild("##setentries", ImVec2(0, -buttonsheight * 2), true);
|
|
// with the previews of the models, as in the node bank
|
|
if (EditorSettings.nodebank_previews() && m_previews != nullptr)
|
|
{
|
|
render_set_cards(edited->id);
|
|
}
|
|
else
|
|
{
|
|
for (int idx = 0; idx < static_cast<int>(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")))
|
|
{
|
|
if (node_selected())
|
|
EditorModelSets.add(setid, {*m_selectedtemplate});
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Remove")) && m_setsentry >= 0)
|
|
{
|
|
EditorModelSets.erase(setid, static_cast<std::size_t>(m_setsentry));
|
|
m_setsentry = -1;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Clear")))
|
|
{
|
|
ImGui::OpenPopup("##clearset");
|
|
}
|
|
if (ImGui::BeginPopup("##clearset"))
|
|
{
|
|
ImGui::TextUnformatted(STR_C("Remove all templates from this set?"));
|
|
if (ImGui::Button(STR_C("Clear##confirm")))
|
|
{
|
|
EditorModelSets.clear(setid);
|
|
m_setsentry = -1;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Cancel")))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// whole node bank group at once
|
|
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Add group")).x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
|
|
auto const groups{group_names()};
|
|
m_setsgroup.id = groups.empty() ? -1 : std::clamp(m_setsgroup.id, 0, static_cast<int>(groups.size()) - 1);
|
|
if (ImGui::BeginCombo("##addgroup", m_setsgroup.id >= 0 ? groups[m_setsgroup.id].c_str() : STR_C("(no node bank groups)")))
|
|
{
|
|
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
|
|
{
|
|
auto const label{groups[idx] + "##addgroup" + std::to_string(idx)};
|
|
if (ImGui::Selectable(label.c_str(), idx == m_setsgroup.id))
|
|
m_setsgroup.id = idx;
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Add group")))
|
|
{
|
|
std::vector<std::string> templates;
|
|
for (auto const *entry : set_entries(m_setsgroup))
|
|
templates.push_back(*entry);
|
|
EditorModelSets.add(setid, templates);
|
|
}
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void nodebank_panel::add_template(const std::string &desc)
|
|
{
|
|
|
|
auto const label{generate_node_label(desc)};
|
|
m_nodebank.push_back({label, std::make_shared<std::string>(desc)});
|
|
|
|
std::ofstream file;
|
|
file.open("nodebank.txt", std::ios_base::out | std::ios_base::app | std::ios_base::binary);
|
|
file << label << " " << desc;
|
|
}
|
|
|
|
const std::string *nodebank_panel::get_active_template()
|
|
{
|
|
return m_selectedtemplate.get();
|
|
}
|
|
|
|
bool nodebank_panel::node_selected() const
|
|
{
|
|
// scenery templates are placed with include directives, the tools which scatter nodes have no use for them
|
|
return m_selectedtemplate && false == m_selectedtemplate->empty() && false == m_selectedtemplate->starts_with(editor_includes::directive_mark);
|
|
}
|
|
|
|
std::vector<std::string> nodebank_panel::group_names() const
|
|
{
|
|
std::vector<std::string> names;
|
|
for (auto const &entry : m_nodebank)
|
|
{
|
|
if (entry.second->empty())
|
|
names.emplace_back(entry.first);
|
|
else if (names.empty())
|
|
names.emplace_back(STR_C("(ungrouped)")); // templates listed before the first header
|
|
}
|
|
return names;
|
|
}
|
|
|
|
std::vector<std::string> nodebank_panel::group_templates(std::size_t const Group) const
|
|
{
|
|
std::vector<std::string> templates;
|
|
std::size_t group{0};
|
|
bool started{false};
|
|
for (auto const &entry : m_nodebank)
|
|
{
|
|
if (entry.second->empty())
|
|
{
|
|
// header: opens the next group, unless it's the very first entry
|
|
if (started)
|
|
++group;
|
|
started = true;
|
|
continue;
|
|
}
|
|
started = true;
|
|
if (group == Group)
|
|
templates.emplace_back(*entry.second);
|
|
else if (group > Group)
|
|
break;
|
|
}
|
|
return templates;
|
|
}
|
|
|
|
std::string nodebank_panel::generate_node_label(std::string Input) const
|
|
{
|
|
|
|
auto tokenizer{cParser(Input)};
|
|
tokenizer.getTokens(9, false); // skip leading tokens
|
|
auto model{tokenizer.getToken<std::string>(false)};
|
|
auto texture{tokenizer.getToken<std::string>(false)};
|
|
replace_slashes(model);
|
|
erase_extension(model);
|
|
replace_slashes(texture);
|
|
return texture == "none" ? model : model + " (" + texture + ")";
|
|
}
|
|
|
|
void functions_panel::update(scene::basic_node const *Node)
|
|
{
|
|
m_node = Node;
|
|
|
|
if (false == is_open)
|
|
{
|
|
return;
|
|
}
|
|
|
|
text_lines.clear();
|
|
m_grouplines.clear();
|
|
|
|
std::string textline;
|
|
|
|
// scenario inspector
|
|
auto const *node{Node};
|
|
auto const &camera{Global.pCamera};
|
|
}
|
|
|
|
void functions_panel::render()
|
|
{
|
|
|
|
if (false == is_open)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
|
|
|
|
if (size.x > 0)
|
|
{
|
|
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
|
|
}
|
|
if (size_min.x > 0)
|
|
{
|
|
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2(size_max.x, size_max.y));
|
|
}
|
|
auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name};
|
|
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
|
|
{
|
|
// header section
|
|
render_controls();
|
|
for (auto const &line : text_lines)
|
|
{
|
|
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void functions_panel::render_controls()
|
|
{
|
|
// order matches rotation_mode
|
|
char const *const modes[] = {STR_C("Random"), STR_C("Fixed"), STR_C("Default (from template)")};
|
|
auto current{static_cast<int>(rot_mode)};
|
|
if (ImGui::Combo(STR_C("Rotation"), ¤t, modes, IM_ARRAYSIZE(modes)))
|
|
{
|
|
rot_mode = static_cast<rotation_mode>(current);
|
|
}
|
|
if (rot_mode == FIXED)
|
|
{
|
|
// ImGui::Checkbox(STR_C("Get rotation from last object"), &rot_from_last);
|
|
ImGui::SliderFloat(STR_C("Rotation value"), &rot_value, 0.0f, 360.0f, STR_C("%.1f deg"));
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
std::pair<char const *, scene::layer_item> const layer_itemlabels[] = {{"models", scene::layer_item::model},
|
|
{"tracks", scene::layer_item::track},
|
|
{"traction", scene::layer_item::traction},
|
|
{"power sources", scene::layer_item::powersource},
|
|
{"memory cells", scene::layer_item::memcell},
|
|
{"event launchers", scene::layer_item::launcher},
|
|
{"events", scene::layer_item::event},
|
|
{"vehicles", scene::layer_item::vehicle},
|
|
{"sounds", scene::layer_item::sound},
|
|
{"terrain shapes", scene::layer_item::shape},
|
|
{"lines", scene::layer_item::lines}};
|
|
|
|
// what the layer holds, a line per kind of item
|
|
std::string layer_content(scene::basic_layer const &Layer)
|
|
{
|
|
std::string content;
|
|
for (auto const &itemlabel : layer_itemlabels)
|
|
{
|
|
auto const count{Layer.items[static_cast<std::size_t>(itemlabel.second)]};
|
|
if (count > 0)
|
|
{
|
|
content += (content.empty() ? "" : "\n") + std::string{STR_C(itemlabel.first)} + ": " + std::to_string(count);
|
|
}
|
|
}
|
|
return content;
|
|
}
|
|
|
|
std::string layer_name(scene::layer_handle const Layer)
|
|
{
|
|
return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : std::string{STR_C("(none)")};
|
|
}
|
|
|
|
// button which can be greyed out, with an explanation shown when it's hovered
|
|
bool action_button(char const *Label, bool const Enabled, std::string const &Reason = std::string())
|
|
{
|
|
if (false == Enabled)
|
|
{
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
|
|
}
|
|
auto const clicked{ImGui::Button(Label) && Enabled};
|
|
if (false == Enabled)
|
|
{
|
|
ImGui::PopStyleVar();
|
|
if (false == Reason.empty() && ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("%s", Reason.c_str());
|
|
}
|
|
}
|
|
return clicked;
|
|
}
|
|
|
|
// text field bound to a string
|
|
bool input_text(char const *Label, std::string &Value)
|
|
{
|
|
std::array<char, 256> buffer{};
|
|
Value.copy(buffer.data(), buffer.size() - 1);
|
|
if (false == ImGui::InputText(Label, buffer.data(), buffer.size()))
|
|
{
|
|
return false;
|
|
}
|
|
Value = buffer.data();
|
|
return true;
|
|
}
|
|
|
|
ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f};
|
|
|
|
} // namespace
|
|
|
|
void itemproperties_panel::render_include()
|
|
{
|
|
auto &include{*m_include};
|
|
include.active = false;
|
|
|
|
ImGui::Text(STR_C("include: %s"), Bezogonkow(include.target).c_str());
|
|
if (false == include.info.name.empty())
|
|
{
|
|
ImGui::TextDisabled("%s", include.info.name.c_str());
|
|
}
|
|
if (false == include.issue.empty())
|
|
{
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
ImGui::TextColored(status_errorcolor, "%s", include.issue.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
return;
|
|
}
|
|
if (include.values.empty())
|
|
{
|
|
ImGui::TextDisabled(STR_C("The template takes no parameters."));
|
|
return;
|
|
}
|
|
|
|
ImGui::PushItemWidth(ImGui::GetFontSize() * 14.0f);
|
|
for (std::size_t idx = 0; idx < include.values.size(); ++idx)
|
|
{
|
|
auto const id{static_cast<int>(idx + 1)};
|
|
auto const lookup{std::find_if(std::begin(include.info.parameters), std::end(include.info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
|
|
auto const described{lookup != std::end(include.info.parameters)};
|
|
auto const role{described ? lookup->role : std::string{"free"}};
|
|
// the label from the description, or what the description tells about the parameter
|
|
auto const label{described && false == lookup->label.empty() ? lookup->label : role != "free" ? role : "(p" + std::to_string(id) + ")"};
|
|
auto &value{include.values[idx]};
|
|
|
|
ImGui::PushID(id);
|
|
char *end{nullptr};
|
|
auto number{std::strtod(value.c_str(), &end)};
|
|
if ((role.starts_with("pos.") || role.starts_with("rot.") || role == "number") && false == value.empty() && *end == '\0')
|
|
{
|
|
if (ImGui::DragScalar(label.c_str(), ImGuiDataType_Double, &number, role.starts_with("pos.") ? 0.05f : 0.5f, nullptr, nullptr, "%.3f"))
|
|
{
|
|
value = editor_includes::number(number);
|
|
include.changed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// applied once the field is left, so the template isn't processed with a half-typed value
|
|
input_text(label.c_str(), value);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
{
|
|
include.changed = true;
|
|
}
|
|
}
|
|
include.active |= ImGui::IsItemActive();
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("(p%d), %s", id, role.c_str());
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
ImGui::TextWrapped(STR_C("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again."));
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
void layers_panel::report(std::string const &Status, bool const Error)
|
|
{
|
|
status = Status;
|
|
status_error = Error;
|
|
}
|
|
|
|
void layers_panel::render()
|
|
{
|
|
if (false == is_open)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// initially next to the toolset window
|
|
ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver);
|
|
auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name};
|
|
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
|
|
{
|
|
if (scene::Layers.empty())
|
|
{
|
|
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
|
|
ImGui::TextDisabled(STR_C("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>"));
|
|
}
|
|
else
|
|
{
|
|
if (false == scene::Layers.listed(m_selected))
|
|
{
|
|
m_selected = null_handle;
|
|
}
|
|
auto const selected{m_selected != null_handle};
|
|
auto const removed{selected && scene::Layers.layer(m_selected).removed};
|
|
std::string reason{STR("Select a layer first")};
|
|
|
|
if (ImGui::Button(STR_C("New...")))
|
|
{
|
|
m_newname[0] = '\0';
|
|
m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1});
|
|
m_popuperror.clear();
|
|
ImGui::OpenPopup(STR_C("New layer"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (removed)
|
|
{
|
|
if (ImGui::Button(STR_C("Restore")))
|
|
{
|
|
scene::Layers.restore(m_selected);
|
|
report(format(STR_C("Layer \"%s\" restored"), layer_name(m_selected).c_str()));
|
|
}
|
|
}
|
|
else if (action_button(STR_C("Remove..."), selected && scene::Layers.can_remove(m_selected, reason), reason))
|
|
{
|
|
ImGui::OpenPopup(STR_C("Remove layer"));
|
|
}
|
|
ImGui::SameLine();
|
|
// the scenario file is what holds the scenery together, it stays where it is
|
|
auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())};
|
|
if (action_button(STR_C("Merge into..."), mergeable, selected && false == removed ? STR("The scenario file can't be merged into another layer") : STR("Select a layer first")))
|
|
{
|
|
// the layer which includes the selected one is the most likely target
|
|
m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent);
|
|
if (false == scene::Layers.can_merge(m_selected, m_mergetarget, reason))
|
|
{
|
|
m_mergetarget = null_handle;
|
|
}
|
|
ImGui::OpenPopup(STR_C("Merge layer"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Save")) && save)
|
|
{
|
|
save();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Writes changed nodes and layers to the scenery files (Ctrl+S)"));
|
|
}
|
|
ImGui::Separator();
|
|
|
|
render_list();
|
|
|
|
if (false == status.empty())
|
|
{
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
if (status_error)
|
|
{
|
|
ImGui::TextColored(status_errorcolor, "%s", status.c_str());
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextUnformatted(status.c_str());
|
|
}
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
|
|
render_popups();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void layers_panel::render_list()
|
|
{
|
|
// the list takes the window except for the room left for the status
|
|
ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f));
|
|
ImGui::TextDisabled(STR_C("visible, locked, active, layer file"));
|
|
|
|
for (auto const handle : scene::Layers.tree())
|
|
{
|
|
// NOTE: copies of the flags, as the calls below can change the layer
|
|
auto const &layer{scene::Layers.layer(handle)};
|
|
auto const isactive{handle == scene::Layers.active()};
|
|
auto const isremoved{layer.removed};
|
|
std::string readonly;
|
|
auto const iswritable{scene::Layers.writable(handle, &readonly)};
|
|
|
|
ImGui::PushID(static_cast<int>(handle));
|
|
if (isremoved)
|
|
{
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
|
|
}
|
|
// active layer receives new nodes, so it can't be hidden nor locked. removed layer is out of the picture altogether
|
|
auto const fixed{isactive || isremoved};
|
|
auto visible{layer.visible};
|
|
if (ImGui::Checkbox("##visible", &visible) && false == fixed)
|
|
{
|
|
scene::Layers.visible(handle, visible);
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("%s", isactive ? STR_C("Active layer is always visible") : STR_C("Show or hide models, tracks and traction of the layer"));
|
|
}
|
|
ImGui::SameLine();
|
|
auto locked{layer.locked};
|
|
if (ImGui::Checkbox("##locked", &locked) && false == fixed)
|
|
{
|
|
scene::Layers.locked(handle, locked);
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("%s", isactive ? STR_C("Active layer can't be locked") : STR_C("Nodes of locked layer can't be selected in the viewport"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("##active", isactive) && false == isactive)
|
|
{
|
|
if (std::string reason; false == scene::Layers.accepts(handle, &reason))
|
|
{
|
|
report(format(STR_C("Layer \"%s\" can't take new nodes"), layer_name(handle).c_str()) + (reason.empty() ? "" : ": " + reason), true);
|
|
}
|
|
else
|
|
{
|
|
scene::Layers.active(handle);
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Active layer: nodes created in the editor are placed in it"));
|
|
}
|
|
ImGui::SameLine();
|
|
auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? std::string{STR_C("binary terrain")} : std::to_string(layer.item_count())) + ")"};
|
|
if (layer.created)
|
|
{
|
|
label += std::string{" "} + STR_C("[new]");
|
|
}
|
|
if (isremoved)
|
|
{
|
|
label += std::string{" "} + STR_C("[removed]");
|
|
}
|
|
else if (false == iswritable)
|
|
{
|
|
label += std::string{" "} + STR_C("[read-only]");
|
|
}
|
|
else if (layer.late)
|
|
{
|
|
label += std::string{" "} + STR_C("[after FirstInit]");
|
|
}
|
|
if (ImGui::Selectable(label.c_str(), handle == m_selected))
|
|
{
|
|
m_selected = handle;
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
std::string content{layer.created ? STR("New layer, its file is created on save") : isremoved ? STR("Dropped from the scenery on save") : STR("Scenery file")};
|
|
if (scene::Layers.valid(layer.parent))
|
|
{
|
|
content += "\n" + format(STR_C("included by: %s"), layer_name(scene::Layers.resolve(layer.parent)).c_str());
|
|
}
|
|
if (false == iswritable)
|
|
{
|
|
content += "\n" + format(STR_C("read-only: %s"), readonly.c_str());
|
|
}
|
|
if (layer.late)
|
|
{
|
|
content += std::string{"\n"} + STR_C("loaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there");
|
|
}
|
|
if (auto const items{layer_content(layer)}; false == items.empty())
|
|
{
|
|
content += "\n" + items;
|
|
}
|
|
ImGui::SetTooltip("%s", content.c_str());
|
|
}
|
|
if (isremoved)
|
|
{
|
|
ImGui::PopStyleVar();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void layers_panel::render_popups()
|
|
{
|
|
if (ImGui::BeginPopupModal(STR_C("New layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
ImGui::TextUnformatted(STR_C("File of the new layer, in the scenery directory:"));
|
|
ImGui::InputTextWithHint("##name", STR_C("name.scm"), m_newname, IM_ARRAYSIZE(m_newname));
|
|
if (ImGui::BeginCombo(STR_C("Included by"), layer_name(m_newparent).c_str()))
|
|
{
|
|
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
|
{
|
|
auto const handle{static_cast<scene::layer_handle>(idx)};
|
|
if (false == scene::Layers.listed(handle) || scene::Layers.layer(handle).removed || scene::Layers.layer(handle).binary)
|
|
{
|
|
continue;
|
|
}
|
|
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_newparent))
|
|
{
|
|
m_newparent = handle;
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (false == m_popuperror.empty())
|
|
{
|
|
ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str());
|
|
}
|
|
if (ImGui::Button(STR_C("Create")))
|
|
{
|
|
auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)};
|
|
if (created != null_handle)
|
|
{
|
|
m_selected = created;
|
|
report(format(STR_C("Layer \"%s\" created and made active. Its file is written on save."), layer_name(created).c_str()));
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Cancel")))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal(STR_C("Remove layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
if (false == scene::Layers.listed(m_selected))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
else
|
|
{
|
|
auto const &layer{scene::Layers.layer(m_selected)};
|
|
auto included{0};
|
|
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
|
{
|
|
auto const handle{static_cast<scene::layer_handle>(idx)};
|
|
if (handle != m_selected && scene::Layers.listed(handle) && scene::Layers.is_descendant(handle, m_selected))
|
|
{
|
|
++included;
|
|
}
|
|
}
|
|
ImGui::Text(STR_C("Remove layer \"%s\" from the scenery?"), layer_name(m_selected).c_str());
|
|
ImGui::TextUnformatted(layer.created ? STR_C("The layer wasn't saved yet, so it leaves no file behind.") : STR_C("Its include directive is erased on save. The file itself stays on the disk."));
|
|
if (included > 0)
|
|
{
|
|
ImGui::Text(STR_C("%d layer(s) it includes are removed with it."), included);
|
|
}
|
|
if (auto const items{layer_content(layer)}; false == items.empty())
|
|
{
|
|
ImGui::Separator();
|
|
ImGui::TextUnformatted(STR_C("The layer holds:"));
|
|
ImGui::TextUnformatted(items.c_str());
|
|
ImGui::TextDisabled(STR_C("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells."));
|
|
}
|
|
if (ImGui::Button(STR_C("Remove")))
|
|
{
|
|
auto const name{layer_name(m_selected)};
|
|
if (scene::Layers.remove(m_selected))
|
|
{
|
|
report(format(STR_C("Layer \"%s\" removed. It can be restored until the scenery is saved."), name.c_str()));
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Cancel")))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal(STR_C("Merge layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
|
{
|
|
if (false == scene::Layers.listed(m_selected))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
else
|
|
{
|
|
ImGui::Text(STR_C("Move content of layer \"%s\" to:"), layer_name(m_selected).c_str());
|
|
std::string reason;
|
|
auto targets{0};
|
|
if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str()))
|
|
{
|
|
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
|
|
{
|
|
auto const handle{static_cast<scene::layer_handle>(idx)};
|
|
if (false == scene::Layers.listed(handle) || false == scene::Layers.can_merge(m_selected, handle, reason))
|
|
{
|
|
continue;
|
|
}
|
|
++targets;
|
|
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_mergetarget))
|
|
{
|
|
m_mergetarget = handle;
|
|
}
|
|
}
|
|
if (targets == 0)
|
|
{
|
|
ImGui::TextDisabled(STR_C("no layer can take it"));
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)};
|
|
auto const merged{action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer"))};
|
|
if (possible && ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip("%s", STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n"
|
|
"Merged into the layer which includes it, the content stays where the include was.\n"
|
|
"Merged into any other layer it's placed at its end, which changes the load order."));
|
|
}
|
|
if (merged)
|
|
{
|
|
auto const name{layer_name(m_selected)};
|
|
auto const target{m_mergetarget};
|
|
if (scene::Layers.merge(m_selected, target))
|
|
{
|
|
report(format(STR_C("Layer \"%s\" merged into \"%s\". The files are changed on save."), name.c_str(), layer_name(target).c_str()));
|
|
m_selected = target;
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Cancel")))
|
|
{
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
void includes_panel::open(std::string const &File)
|
|
{
|
|
m_file = File;
|
|
m_statuserror = false;
|
|
m_parameters = 0;
|
|
std::string error;
|
|
m_loaded = editor_includes::load(File, m_info, error);
|
|
if (false == m_loaded)
|
|
{
|
|
m_status = error;
|
|
m_statuserror = true;
|
|
return;
|
|
}
|
|
auto const described{false == m_info.name.empty() || false == m_info.parameters.empty()};
|
|
// every parameter the template uses gets an entry, described or not
|
|
m_parameters = editor_includes::parameter_count(File);
|
|
for (auto id = 1; id <= m_parameters; ++id)
|
|
{
|
|
m_info.parameter(id);
|
|
}
|
|
m_status = format(STR_P("The template uses %d parameter.", "The template uses %d parameters.", m_parameters), m_parameters);
|
|
if (false == described)
|
|
{
|
|
// starting point for a new description
|
|
editor_includes::suggest(File, m_info);
|
|
m_status += std::string{" "} + STR_C("It has no description yet; the roles are a suggestion based on how the template uses the parameters.");
|
|
}
|
|
}
|
|
|
|
void includes_panel::render_list()
|
|
{
|
|
EditorIncludes.start();
|
|
auto const &entries{EditorIncludes.entries()};
|
|
|
|
ImGui::PushItemWidth(ImGui::GetFontSize() * 12.0f);
|
|
auto changed{ImGui::InputTextWithHint("##filter", STR_C("Search templates"), m_filter, IM_ARRAYSIZE(m_filter))};
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
changed |= ImGui::Checkbox(STR_C("Described"), &m_describedonly);
|
|
if (false == scene::Layers.templates().empty())
|
|
{
|
|
// known only for scenery opened for editing
|
|
ImGui::SameLine();
|
|
changed |= ImGui::Checkbox(STR_C("Used by the scenery"), &m_usedonly);
|
|
}
|
|
ImGui::SameLine();
|
|
if (EditorIncludes.searching())
|
|
{
|
|
ImGui::TextDisabled("%s", STR_C("Looking for the templates..."));
|
|
}
|
|
else if (ImGui::Button(STR_C("Rescan")))
|
|
{
|
|
EditorIncludes.rescan();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Looks through the scenery directory again, for the templates added or changed outside of the editor"));
|
|
}
|
|
|
|
if (changed || m_listrevision != EditorIncludes.revision())
|
|
{
|
|
m_listrevision = EditorIncludes.revision();
|
|
m_listed.clear();
|
|
m_described = 0;
|
|
std::string const filter{m_filter};
|
|
auto const &used{scene::Layers.templates()};
|
|
for (std::size_t idx = 0; idx < entries.size(); ++idx)
|
|
{
|
|
auto const &entry{entries[idx]};
|
|
m_described += (entry.described ? 1 : 0);
|
|
if ((m_describedonly && false == entry.described) || (m_usedonly && false == used.empty() && used.count(entry.file) == 0))
|
|
{
|
|
continue;
|
|
}
|
|
if (false == filter.empty() && false == contains(entry.file, filter) && false == contains(entry.name, filter) && false == contains(entry.category, filter))
|
|
{
|
|
continue;
|
|
}
|
|
m_listed.emplace_back(idx);
|
|
}
|
|
}
|
|
ImGui::TextDisabled(STR_C("%d template(s) in the scenery directory, %d described, %d ready for the node bank"), static_cast<int>(entries.size()), m_described,
|
|
static_cast<int>(EditorIncludes.ready().size()));
|
|
|
|
ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 9.0f), true);
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(static_cast<int>(m_listed.size()));
|
|
while (clipper.Step())
|
|
{
|
|
for (auto row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
|
{
|
|
auto const &entry{entries[m_listed[row]]};
|
|
auto const marker{entry.complete ? std::string{STR_C("[ready]")} + " " : entry.described ? std::string{STR_C("[incomplete]")} + " " : std::string{}};
|
|
auto const label{marker + Bezogonkow(entry.file) + (entry.name.empty() ? "" : " - " + entry.name) + "##" + std::to_string(m_listed[row])};
|
|
if (false == entry.described)
|
|
{
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
}
|
|
auto const clicked{list_item(label.c_str(), entry.file == m_file)};
|
|
if (false == entry.described)
|
|
{
|
|
ImGui::PopStyleColor();
|
|
}
|
|
if (false == entry.complete && false == entry.issue.empty() && ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Not in the node bank: %s"), entry.issue.c_str());
|
|
}
|
|
if (clicked)
|
|
{
|
|
open(entry.file);
|
|
}
|
|
}
|
|
}
|
|
if (m_listed.empty())
|
|
{
|
|
ImGui::TextDisabled("%s", entries.empty() ? STR_C("(no .inc files in the scenery directory)") : STR_C("(no match)"));
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void includes_panel::render_description()
|
|
{
|
|
ImGui::Separator();
|
|
ImGui::Text("%s", Bezogonkow(m_file).c_str());
|
|
input_text(STR_C("Name"), m_info.name);
|
|
input_text(STR_C("Category"), m_info.category);
|
|
input_text(STR_C("Description"), m_info.description);
|
|
|
|
ImGui::Columns(4, "##parameters", false);
|
|
ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x);
|
|
ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f);
|
|
ImGui::TextDisabled(STR_C("param"));
|
|
ImGui::NextColumn();
|
|
ImGui::TextDisabled(STR_C("role"));
|
|
ImGui::NextColumn();
|
|
ImGui::TextDisabled(STR_C("label"));
|
|
ImGui::NextColumn();
|
|
ImGui::TextDisabled(STR_C("default"));
|
|
ImGui::NextColumn();
|
|
for (auto ¶meter : m_info.parameters)
|
|
{
|
|
ImGui::PushID(parameter.id);
|
|
ImGui::AlignTextToFramePadding();
|
|
// parameters which keep the template out of the node bank stand out
|
|
if (parameter.id <= m_parameters && parameter.value.empty() && false == editor_includes::automatic(parameter.role))
|
|
{
|
|
ImGui::TextColored(status_errorcolor, "(p%d)", parameter.id);
|
|
}
|
|
else
|
|
{
|
|
ImGui::Text("(p%d)", parameter.id);
|
|
}
|
|
ImGui::NextColumn();
|
|
ImGui::PushItemWidth(-1.0f);
|
|
if (ImGui::BeginCombo("##role", parameter.role.c_str()))
|
|
{
|
|
for (auto const &role : editor_includes::roles)
|
|
{
|
|
if (ImGui::Selectable(role.c_str(), role == parameter.role))
|
|
{
|
|
parameter.role = role;
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::NextColumn();
|
|
ImGui::PushItemWidth(-1.0f);
|
|
input_text("##label", parameter.label);
|
|
ImGui::PopItemWidth();
|
|
ImGui::NextColumn();
|
|
ImGui::PushItemWidth(-1.0f);
|
|
input_text("##default", parameter.value);
|
|
ImGui::PopItemWidth();
|
|
ImGui::NextColumn();
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::Columns(1);
|
|
|
|
// what the node bank is going to make of the description
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
if (std::string issue; editor_includes::complete(m_info, m_parameters, &issue))
|
|
{
|
|
ImGui::TextUnformatted(STR_C("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)."));
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextColored(status_errorcolor, STR_C("Not for the node bank yet: %s."), issue.c_str());
|
|
}
|
|
ImGui::PopTextWrapPos();
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n"
|
|
"Insert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\n"
|
|
"The other parameters receive their default values."));
|
|
}
|
|
|
|
if (ImGui::Button(STR_C("Save to file")))
|
|
{
|
|
std::string error;
|
|
m_statuserror = false == editor_includes::save(m_file, m_info, error);
|
|
m_status = m_statuserror ? error : format(STR_C("Description saved in \"%s\""), Bezogonkow(m_file).c_str());
|
|
if (false == m_statuserror)
|
|
{
|
|
EditorIncludes.update(m_file);
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Reload")))
|
|
{
|
|
open(m_file);
|
|
}
|
|
if (ImGui::TreeNode(STR_C("More settings")))
|
|
{
|
|
if (ImGui::Button(STR_C("Suggest roles")))
|
|
{
|
|
editor_includes::suggest(m_file, m_info);
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Add parameter")))
|
|
{
|
|
m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1);
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
}
|
|
|
|
void includes_panel::render()
|
|
{
|
|
if (false == is_open)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// next to the layers window where the screen is wide enough for both
|
|
auto const position{ImVec2S(970, 60)};
|
|
auto const windowsize{ImVec2S(600, 600)};
|
|
ImGui::SetNextWindowPos(position.x + windowsize.x <= ImGui::GetIO().DisplaySize.x ? position : ImVec2S(560, 110), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(windowsize, ImGuiCond_FirstUseEver);
|
|
auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name};
|
|
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
|
|
{
|
|
ImGui::TextDisabled("%s", STR_C("Describe a template to add it to the node bank"));
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
std::string about{STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; "
|
|
"the description is stored in the template file, as //$e comment lines.")};
|
|
if (false == Global.editor_session)
|
|
{
|
|
about += std::string{"\n"} + STR_C("The node bank offers the templates in a scenery opened for editing (-edit).");
|
|
}
|
|
ImGui::SetTooltip("%s", about.c_str());
|
|
}
|
|
|
|
render_list();
|
|
if (m_loaded)
|
|
{
|
|
render_description();
|
|
}
|
|
if (false == m_status.empty())
|
|
{
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
if (m_statuserror)
|
|
{
|
|
ImGui::TextColored(status_errorcolor, "%s", m_status.c_str());
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextUnformatted(m_status.c_str());
|
|
}
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|