diff --git a/CMakeLists.txt b/CMakeLists.txt index 1882b258..43756f18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,6 +184,7 @@ set(SOURCES "input/editorkeyboardinput.cpp" "editor/editorSettings.cpp" "editor/editorModelSets.cpp" +"editor/editorTrackPresets.cpp" "editor/editorIncludeInfo.cpp" "editor/editorTerrain.cpp" "editor/editorTrack.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index e5e345d3..6f3fba23 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -28,6 +28,7 @@ extern char **environ; #include "application/application.h" #include "editor/editorSettings.hpp" #include "editor/editorModelSets.hpp" +#include "editor/editorTrackPresets.hpp" #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" @@ -534,6 +535,7 @@ editor_mode::editor_mode() { ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); ui()->set_open_scenery([this]() { m_openscenery_asked = true; }); ui()->set_edit_menu([this]() { render_edit_menu(); }); + ui()->set_tools_menu([this]() { render_track_presets_menu(); }); ui()->set_menu_options([this]() { render_object_menu(); render_view_menu(); @@ -639,6 +641,7 @@ bool editor_mode::init() EditorSettings.load(); Global.EditorOrthoBallast = EditorSettings.top_ballast(); EditorModelSets.load(); + EditorTrackPresets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); return m_input.init(); } diff --git a/application/editormode.h b/application/editormode.h index bd56cbe4..62e9a09f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1796,6 +1796,15 @@ class editor_mode : public application_mode, private editor_track::observer void render_track_style(std::vector const &Tracks); void copy_track_style(TTrack const &Track); std::size_t paste_track_style(std::vector const &Tracks); + // which groups of the properties are pasted or put on the tracks by a preset, in the order of the groups of editormodetrackprops.cpp + using track_groups = std::array; + // the values of the properties of the chosen groups put on the tracks, in one step of the undo. returns: the number of the + // tracks changed, none when no value is of the chosen groups + std::optional apply_track_values(std::vector> const &Values, track_groups const &Groups, std::vector const &Tracks, char const *What); + // presets of the properties of the tracks (editorTrackPresets), made from a track and put on the selected ones + void render_track_presets(std::vector const &Tracks); + void render_track_presets_menu(); + std::size_t apply_track_preset(int const Id, std::vector const &Tracks); std::vector similar_tracks(int const Property, std::vector const &Tracks) const; // values of the properties over the selected tracks, each with the number of the tracks having it struct track_values @@ -1841,12 +1850,19 @@ class editor_mode : public application_mode, private editor_track::observer { std::string from; std::vector> values; - // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs - std::array groups{false, false, false, false, true, true, true, true, false, true}; + // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs, sleeper placement + track_groups groups{false, false, false, false, true, true, true, true, false, true, true}; bool open{false}; bool picking{false}; // the next click on a track in the view copies its properties }; track_style m_track_style; + struct track_presets_panel + { + int selected{0}; // id of the preset chosen in the inspector + int nameid{0}; // the preset whose name is in the field + char name[128]{}; + }; + track_presets_panel m_track_presets; std::vector> m_track_similar; TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks int m_track_list_hover_frame{-1}; diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 0cab1a72..c6fc8b46 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorTrackPresets.hpp" #include "rendering/renderer.h" #include "world/Track.h" @@ -224,10 +225,13 @@ enum group : int sleepers, ride_sound, frogs, + sleeper_placement, // spacing and shift of the sleepers and of the trackbed under them, apart from their models group_count }; -char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails"}; +char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails", "Sleeper placement"}; +// the groups as the presets keep them in their file, by names which don't change with the order of the groups +char const *const kGroupKeys[group_count] = {"speed", "environment", "overhead", "circuits", "rails", "trackbed", "bedshape", "sleepers", "ride", "frogs", "sleeperplacement"}; struct track_property { @@ -632,13 +636,13 @@ std::vector make_properties() }; } } - add_float(list, "sleeperspacing", section::look_sleepers, group::sleepers, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true); + add_float(list, "sleeperspacing", section::look_sleepers, group::sleeper_placement, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true); char const *const offsets[][3] = {{"sleeperside", "Sleeper shift sideways (m)", "Shift of each model sideways, to the right of the track"}, {"sleeperalong", "Sleeper shift along (m)", "Shift of each model along the track"}, {"sleeperup", "Sleeper shift up (m)", "Shift of each model up"}}; for (int axis = 0; axis < 3; ++axis) { - auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleepers, offsets[axis][1], offsets[axis][2])}; + auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleeper_placement, offsets[axis][1], offsets[axis][2])}; property.rebuild = true; property.get = [axis](TTrack const &Track) { return decimal(Track.m_sleeper_offset[axis], 3); }; property.set = [axis](TTrack &Track, std::string const &Text) { @@ -649,7 +653,7 @@ std::vector make_properties() return true; }; } - add_float(list, "bedshift", section::look_sleepers, group::sleepers, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true); + add_float(list, "bedshift", section::look_sleepers, group::sleeper_placement, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true); // ride and sound add_float(list, "friction", section::ride, group::ride_sound, "Friction", "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip", &TTrack::fFriction, 3, 0.f, false); @@ -750,6 +754,62 @@ void row_tooltip(track_property const &Property, value_counts const &Values, std ImGui::EndTooltip(); } +// the values of the properties of the track, as they're copied and kept by the presets +std::vector> values_of_track(TTrack const &Track) +{ + std::vector> values; + for (auto const &property : properties()) + if (property.applies(Track)) + values.emplace_back(property.key, property.get(Track)); + return values; +} + +// the groups some of the values are of +std::array groups_in(std::vector> const &Values) +{ + std::array groups{}; + for (auto const &[key, value] : Values) + for (auto const &property : properties()) + if (key == property.key) + groups[property.group] = true; + return groups; +} + +// the groups the preset overwrites, from the names it keeps them by +std::array groups_of(editor_track_preset const &Preset) +{ + std::array groups{}; + for (auto const &key : Preset.groups) + for (int group = 0; group < group_count; ++group) + if (key == kGroupKeys[group]) + groups[group] = true; + return groups; +} + +std::vector group_keys(std::array const &Groups) +{ + std::vector keys; + for (int group = 0; group < group_count; ++group) + if (Groups[group]) + keys.emplace_back(kGroupKeys[group]); + return keys; +} + +// what the preset puts on the tracks, for its tooltip +std::string preset_summary(editor_track_preset const &Preset) +{ + auto const groups{groups_of(Preset)}; + auto const available{groups_in(Preset.values)}; + std::string overwrites; + for (int group = 0; group < group_count; ++group) + if (groups[group] && available[group]) + overwrites += (overwrites.empty() ? "" : ", ") + std::string{STR_C(kGroupLabels[group])}; + auto text{overwrites.empty() ? std::string{STR_C("Overwrites nothing: choose what it overwrites in the inspector")} : format(STR_C("Overwrites: %s"), overwrites.c_str())}; + if (false == Preset.from.empty()) + text += "\n" + format(STR_C("Taken from %s"), Preset.from.c_str()); + return text; +} + } // namespace std::vector editor_mode::selected_tracks() @@ -1288,6 +1348,7 @@ void editor_mode::render_track_properties(std::vector const &Tracks) if (any_in(section::look_sleepers, false) && ImGui::TreeNode(STR_C("Sleeper models"))) { rows(section::look_sleepers, false); + render_track_presets(Tracks); ImGui::TreePop(); } } @@ -1298,50 +1359,230 @@ void editor_mode::render_track_properties(std::vector const &Tracks) void editor_mode::copy_track_style(TTrack const &Track) { auto &style{m_track_style}; - style.values.clear(); style.from = name_of(Track); - for (auto const &property : properties()) - if (property.applies(Track)) - style.values.emplace_back(property.key, property.get(Track)); + style.values = values_of_track(Track); ui()->set_status(format(STR_C("Properties of %s copied: Ctrl+Shift+V pastes them on the selected tracks"), style.from.c_str()), false); } std::size_t editor_mode::paste_track_style(std::vector const &Tracks) { - static_assert(group_count == std::tuple_size_v, "a choice for each group of the pasted properties"); auto const &style{m_track_style}; if (style.values.empty() || Tracks.empty()) return 0; + auto const changed{apply_track_values(style.values, style.groups, Tracks, "Pasted properties")}; + if (false == changed.has_value()) + { + ui()->set_status(STR_C("Nothing chosen to paste"), true); + return 0; + } + if (Tracks.size() == 1) + ui()->set_status(*changed > 0 ? format(STR_C("Properties of %s pasted, Ctrl+Z takes them back"), style.from.c_str()) : std::string{STR_C("The track has these properties already")}, false); + return *changed; +} + +std::optional editor_mode::apply_track_values(std::vector> const &Values, track_groups const &Groups, std::vector const &Tracks, char const *What) +{ + static_assert(group_count == std::tuple_size_v, "a choice for each group of the pasted properties"); auto const &list{properties()}; std::vector> pasted; bool rebuild{false}; for (auto const late : {false, true}) - for (auto const &[key, value] : style.values) + for (auto const &[key, value] : Values) { auto const index{track_property_index(key)}; if (index < 0) continue; auto const &property{list[index]}; - if (property.late != late || false == style.groups[property.group]) + if (property.late != late || false == Groups[property.group]) continue; pasted.emplace_back(&property, value); rebuild |= property.rebuild; } if (pasted.empty()) - { - ui()->set_status(STR_C("Nothing chosen to paste"), true); - return 0; - } - auto const changed{tracks_apply(Tracks, [&](TTrack &Track) { + return std::nullopt; + return tracks_apply(Tracks, [&](TTrack &Track) { bool any{false}; for (auto const &[property, value] : pasted) if (property->applies(Track)) any |= property->set(Track, value); return any; - }, rebuild, "Pasted properties")}; - if (Tracks.size() == 1) - ui()->set_status(changed > 0 ? format(STR_C("Properties of %s pasted, Ctrl+Z takes them back"), style.from.c_str()) : std::string{STR_C("The track has these properties already")}, false); - return changed; + }, rebuild, What); +} + +std::size_t editor_mode::apply_track_preset(int const Id, std::vector const &Tracks) +{ + auto const *preset{EditorTrackPresets.find(Id)}; + if (preset == nullptr || Tracks.empty()) + return 0; + auto const name{preset->name}; + auto const changed{apply_track_values(preset->values, groups_of(*preset), Tracks, "Applied a track preset")}; + if (false == changed.has_value()) + { + ui()->set_status(format(STR_C("The preset \"%s\" overwrites nothing: choose what it overwrites"), name.c_str()), true); + return 0; + } + if (*changed > 0) + ui()->set_status(format(STR_P("Preset \"%s\" applied on %d track, Ctrl+Z takes it back", "Preset \"%s\" applied on %d tracks, Ctrl+Z takes it back", static_cast(*changed)), name.c_str(), static_cast(*changed)), false); + else + ui()->set_status(STR_C("The tracks have the properties of the preset already"), false); + return *changed; +} + +void editor_mode::render_track_presets(std::vector const &Tracks) +{ + if (Tracks.empty() || false == ImGui::TreeNode(STR_C("Presets"))) + return; + auto &panel{m_track_presets}; + auto const &presets{EditorTrackPresets.presets()}; + if (EditorTrackPresets.find(panel.selected) == nullptr) + panel.selected = presets.empty() ? 0 : presets.front().id; + auto const single{Tracks.size() == 1}; + auto const *selected{EditorTrackPresets.find(panel.selected)}; + + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::BeginCombo("##trackpreset", selected != nullptr ? selected->name.c_str() : STR_C("No presets yet"))) + { + for (auto const &preset : presets) + { + if (ImGui::Selectable(preset.name.c_str(), preset.id == panel.selected)) + panel.selected = preset.id; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", preset_summary(preset).c_str()); + } + ImGui::EndCombo(); + } + if (selected != nullptr && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", preset_summary(*selected).c_str()); + + auto const width{button_width(2)}; + ImGui::BeginDisabled(selected == nullptr); + if (ImGui::Button(format(STR_P("Apply on %d track", "Apply on %d tracks", static_cast(Tracks.size())), static_cast(Tracks.size())).c_str(), ImVec2(width, 0.0f))) + apply_track_preset(panel.selected, Tracks); + ImGui::EndDisabled(); + item_tooltip("Puts the values of the preset on the selected tracks, of what it overwrites; Ctrl+Z takes them back.\nTools in the menu applies the presets too"); + ImGui::SameLine(); + ImGui::BeginDisabled(false == single); + if (ImGui::Button(STR_C("New from the track"), ImVec2(width, 0.0f))) + { + // rails, trackbed, its shape, sleepers, frogs and sleeper placement at first, the look of the track + std::array groups{}; + for (auto const group : {group::rails, group::trackbed, group::trackbed_shape, group::sleepers, group::frogs, group::sleeper_placement}) + groups[group] = true; + panel.selected = EditorTrackPresets.create(STR_C("New preset"), name_of(*Tracks.front()), values_of_track(*Tracks.front()), group_keys(groups)); + panel.nameid = 0; + ui()->set_status(format(STR_C("Properties of %s saved as a preset: give it a name and choose what it overwrites"), name_of(*Tracks.front()).c_str()), false); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C(single ? "Saves the properties of this track as a new preset" : "Select one track to save its properties as a preset")); + + // pointers to the presets don't outlive a change of their list + selected = EditorTrackPresets.find(panel.selected); + if (selected == nullptr) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("A preset keeps the properties of a track: its rails, trackbed, sleepers... to put them on other tracks,\nwith the choice of what it overwrites")); + ImGui::PopTextWrapPos(); + ImGui::TreePop(); + return; + } + auto const id{selected->id}; + + // the name + if (panel.nameid != id) + { + std::snprintf(panel.name, sizeof(panel.name), "%s", selected->name.c_str()); + panel.nameid = 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("##presetname", panel.name, IM_ARRAYSIZE(panel.name), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Rename")) || entered) + { + EditorTrackPresets.rename(id, panel.name); + panel.nameid = 0; // reload, the name could have been adjusted to stay unique + selected = EditorTrackPresets.find(id); + } + + // what it overwrites, of the groups it has values of + ImGui::TextDisabled("%s", STR_C("Overwrites:")); + // its own id scope: the group checkboxes share their labels with the sections of the properties + ImGui::PushID("presetgroups"); + auto const available{groups_in(selected->values)}; + auto groups{groups_of(*selected)}; + auto changed{false}; + int column{0}; + auto const half{ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.5f}; + for (int group = 0; group < group_count; ++group) + { + if (false == available[group]) + continue; + if (column % 2 == 1) + ImGui::SameLine(half); + changed |= ImGui::Checkbox(STR_C(kGroupLabels[group]), &groups[group]); + ++column; + } + ImGui::PopID(); + if (changed) + { + // the groups it has no values of are kept as they were, for a track they're taken again from + auto kept{groups_of(*selected)}; + for (int group = 0; group < group_count; ++group) + if (available[group]) + kept[group] = groups[group]; + EditorTrackPresets.set_groups(id, group_keys(kept)); + selected = EditorTrackPresets.find(id); + } + + ImGui::BeginDisabled(false == single); + if (ImGui::Button(STR_C("Update from the track"), ImVec2(width, 0.0f))) + { + EditorTrackPresets.update(id, name_of(*Tracks.front()), values_of_track(*Tracks.front())); + ui()->set_status(format(STR_C("Preset \"%s\" takes the properties of %s now"), selected->name.c_str(), name_of(*Tracks.front()).c_str()), false); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C(single ? "The preset takes the properties of this track in place of the ones it had; what it overwrites stays" : "Select one track to take its properties into the preset")); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Delete"), ImVec2(width, 0.0f))) + ImGui::OpenPopup("##deletepreset"); + if (ImGui::BeginPopup("##deletepreset")) + { + ImGui::Text(STR_C("Delete preset \"%s\"?"), selected->name.c_str()); + if (ImGui::Button(STR_C("Delete##confirm"))) + { + EditorTrackPresets.remove(id); + panel.selected = 0; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::TreePop(); +} + +void editor_mode::render_track_presets_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Track presets"))) + return; + auto const &presets{EditorTrackPresets.presets()}; + if (presets.empty()) + ImGui::TextDisabled("%s", STR_C("No presets: save one in the inspector of a track, Look, Sleeper models, Presets")); + auto const tracks{selected_tracks()}; + int chosen{0}; + for (auto const &preset : presets) + { + if (ImGui::MenuItem(preset.name.c_str(), nullptr, false, false == tracks.empty())) + chosen = preset.id; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", tracks.empty() ? STR_C("Select the tracks to apply the preset on first") : (format(STR_P("Applies the preset on %d selected track", "Applies the preset on %d selected tracks", static_cast(tracks.size())), static_cast(tracks.size())) + "\n" + preset_summary(preset)).c_str()); + } + ImGui::EndMenu(); + if (chosen != 0) + apply_track_preset(chosen, tracks); } void editor_mode::render_track_style(std::vector const &Tracks) @@ -1614,7 +1855,7 @@ editor_mode::selftest_result editor_mode::selftest_props(std::string const &Comm } else if (action == "groups") { - // the groups pasted, as their numbers: 0 speed ... 9 frogs + // the groups pasted, as their numbers: 0 speed ... 9 frogs, 10 sleeper placement m_track_style.groups.fill(false); for (int group; Words >> group;) if (group >= 0 && group < group_count) diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index faf4df3b..d4d4d60c 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -989,6 +989,15 @@ void editor_ui::render_menu_contents() m_menuoptions(); } +// the tools of the editor mode, in the tools menu after the common ones (logging, screenshot) +void editor_ui::render_tools_menu() +{ + if (!m_toolsmenu) + return; + ImGui::Separator(); + m_toolsmenu(); +} + // the windows of the editor and their layout, in the windows menu after the common ones (log, hierarchy, history, settings) void editor_ui::render_windows_menu() { diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f405bad5..49c94ee3 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -76,6 +76,11 @@ class editor_ui : public ui_layer { m_editmenu = std::move(Renderer); } + // draws the items of the editor mode in the tools menu, after the common ones + void set_tools_menu(std::function Renderer) + { + m_toolsmenu = std::move(Renderer); + } // draws the fields of work of the editor mode in a row above the toolbar void set_workspace_bar(std::function Renderer) { @@ -170,6 +175,7 @@ class editor_ui : public ui_layer void scene_row(std::size_t const Entry); void render_menu_contents() override; void render_windows_menu() override; + void render_tools_menu() override; int palette() const override; void render_dockspace() override; void render_() override; @@ -203,6 +209,7 @@ class editor_ui : public ui_layer std::function m_toolbaroptions; std::function m_workspacebar; std::function m_editmenu; + std::function m_toolsmenu; bool m_layoutreset{false}; // the default layout is to be built again on the next frame bool m_tooloptionsopen{true}; bool m_inspectoropen{true}; diff --git a/application/uilayer.cpp b/application/uilayer.cpp index d7f99d3c..998ea46c 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -696,6 +696,7 @@ void ui_layer::render_menu_contents() if (ImGui::MenuItem(STR_C("Screenshot"), "PrtScr")) Application.queue_screenshot(); + render_tools_menu(); ImGui::EndMenu(); } if (ImGui::BeginMenu(STR_C("Windows"))) diff --git a/application/uilayer.h b/application/uilayer.h index 7b68646f..462c8dc1 100644 --- a/application/uilayer.h +++ b/application/uilayer.h @@ -143,6 +143,8 @@ protected: virtual void render_menu_contents(); // items of the windows menu which belong to the ui of a mode, after the common ones virtual void render_windows_menu() {} + // items of the tools menu which belong to the ui of a mode, after the common ones + virtual void render_tools_menu() {} // colours this ui is drawn in, see imgui_colors() virtual int palette() const { diff --git a/editor/editorTrackPresets.cpp b/editor/editorTrackPresets.cpp new file mode 100644 index 00000000..f41506c0 --- /dev/null +++ b/editor/editorTrackPresets.cpp @@ -0,0 +1,233 @@ +/* +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 "editor/editorTrackPresets.hpp" +#include "utilities/Logs.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include + +editorTrackPresets EditorTrackPresets; + +// file layout: +// preset +// from +// groups ... +// value +// ... +// endpreset +namespace +{ +namespace fs = std::filesystem; + +fs::path presets_path() +{ + fs::path p = user_config_path("eu07_editor_track_presets.txt"); + return p.empty() ? fs::path("eu07_editor_track_presets.txt") : p; +} + +std::string trimmed(std::string Text) +{ + auto const first = Text.find_first_not_of(" \t"); + if (first == std::string::npos) + return {}; + auto const last = Text.find_last_not_of(" \t"); + return Text.substr(first, last - first + 1); +} + +// one value per line in the file +std::string single_line(std::string Text) +{ + std::replace_if(Text.begin(), Text.end(), [](char const Character) { return Character == '\r' || Character == '\n' || Character == '\t'; }, ' '); + return trimmed(Text); +} + +// the line past its first word, when it starts with the word +bool keyword(std::string const &Line, char const *Word, std::string &Rest) +{ + auto const length{std::char_traits::length(Word)}; + if (Line.compare(0, length, Word) != 0 || (Line.size() > length && Line[length] != ' ')) + return false; + Rest = Line.size() > length ? trimmed(Line.substr(length + 1)) : std::string{}; + return true; +} +} // namespace + +bool editorTrackPresets::load() +{ + m_presets.clear(); + + const fs::path path = presets_path(); + std::error_code ec; + if (!fs::exists(path, ec)) + return false; + + std::ifstream stream(path, std::ios_base::in | std::ios_base::binary); + if (!stream.is_open()) + return false; + + editor_track_preset *current{nullptr}; + std::string line; + std::string rest; + while (std::getline(stream, line)) + { + line = single_line(line); + if (line.empty() || line.rfind("//", 0) == 0) + continue; + + // a preset header also closes a preset with missing "endpreset" + if (keyword(line, "preset", rest)) + { + editor_track_preset preset; + preset.id = m_nextid++; + preset.name = unique_name(rest); + m_presets.push_back(std::move(preset)); + current = &m_presets.back(); + continue; + } + if (line == "endpreset") + { + current = nullptr; + continue; + } + if (current == nullptr) + continue; + if (keyword(line, "from", rest)) + current->from = rest; + else if (keyword(line, "groups", rest)) + { + std::istringstream words{rest}; + current->groups.clear(); + for (std::string group; words >> group;) + current->groups.push_back(group); + } + else if (keyword(line, "value", rest) && false == rest.empty()) + { + auto const split{rest.find(' ')}; + if (split == std::string::npos) + current->values.emplace_back(rest, std::string{}); + else + current->values.emplace_back(rest.substr(0, split), trimmed(rest.substr(split + 1))); + } + } + return true; +} + +bool editorTrackPresets::save() const +{ + const fs::path path = presets_path(); + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + + std::ofstream stream(path, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary); + if (!stream.is_open()) + { + ErrorLog("failed to save editor track presets"); + return false; + } + + stream << "// MaSzyna editor: user-defined presets of the track properties\n"; + for (auto const &preset : m_presets) + { + stream << "\npreset " << preset.name << "\n"; + if (false == preset.from.empty()) + stream << "from " << preset.from << "\n"; + stream << "groups"; + for (auto const &group : preset.groups) + stream << ' ' << group; + stream << "\n"; + for (auto const &[key, value] : preset.values) + stream << "value " << key << ' ' << single_line(value) << "\n"; + stream << "endpreset\n"; + } + return true; +} + +editor_track_preset const *editorTrackPresets::find(int const Id) const +{ + auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; }); + return lookup != m_presets.end() ? &(*lookup) : nullptr; +} + +editor_track_preset *editorTrackPresets::find_(int const Id) +{ + auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; }); + return lookup != m_presets.end() ? &(*lookup) : nullptr; +} + +int editorTrackPresets::create(std::string const &Name, std::string const &From, std::vector> const &Values, std::vector const &Groups) +{ + editor_track_preset preset; + preset.id = m_nextid++; + preset.name = unique_name(Name); + preset.from = single_line(From); + preset.values = Values; + preset.groups = Groups; + m_presets.push_back(std::move(preset)); + save(); + return m_presets.back().id; +} + +void editorTrackPresets::remove(int const Id) +{ + auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; }); + if (lookup == m_presets.end()) + return; + m_presets.erase(lookup); + save(); +} + +void editorTrackPresets::rename(int const Id, std::string const &Name) +{ + auto *preset = find_(Id); + if (preset == nullptr) + return; + auto const name = unique_name(Name, Id); + if (name == preset->name) + return; + preset->name = name; + save(); +} + +void editorTrackPresets::update(int const Id, std::string const &From, std::vector> const &Values) +{ + auto *preset = find_(Id); + if (preset == nullptr) + return; + preset->from = single_line(From); + preset->values = Values; + save(); +} + +void editorTrackPresets::set_groups(int const Id, std::vector const &Groups) +{ + auto *preset = find_(Id); + if (preset == nullptr || preset->groups == Groups) + return; + preset->groups = Groups; + save(); +} + +std::string editorTrackPresets::unique_name(std::string const &Base, int const IgnoreId) const +{ + auto base = single_line(Base); + if (base.empty()) + base = "New preset"; + auto const taken = [&](std::string const &Name) { + return std::any_of(m_presets.begin(), m_presets.end(), [&](auto const &Preset) { return Preset.id != IgnoreId && Preset.name == Name; }); + }; + auto name = base; + for (int suffix = 2; taken(name); ++suffix) + name = base + " " + std::to_string(suffix); + return name; +} diff --git a/editor/editorTrackPresets.hpp b/editor/editorTrackPresets.hpp new file mode 100644 index 00000000..32a24778 --- /dev/null +++ b/editor/editorTrackPresets.hpp @@ -0,0 +1,58 @@ +/* +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/. +*/ + +#pragma once + +#include +#include +#include + +// user-defined presets of the properties of tracks (rails, trackbed, sleepers...), taken from a track and put on others +// with the choice of what they overwrite. stored next to the user's ini files, like the model sets +struct editor_track_preset +{ + int id{0}; // runtime identifier, stable while the editor runs (renames don't break references) + std::string name; + std::string from; // name of the track the values were taken from + // the values of the properties as the track inspector reads and writes them, by their keys + std::vector> values; + // the groups of the properties the preset overwrites, by their keys; the other values of it are kept but left out + std::vector groups; +}; + +class editorTrackPresets +{ + public: + bool load(); + bool save() const; + + std::vector const &presets() const + { + return m_presets; + } + editor_track_preset const *find(int Id) const; + // all modifying methods save the presets right away + // returns: id of the new preset + int create(std::string const &Name, std::string const &From, std::vector> const &Values, std::vector const &Groups); + void remove(int Id); + void rename(int Id, std::string const &Name); + // the values taken again, from another track or the same one changed + void update(int Id, std::string const &From, std::vector> const &Values); + void set_groups(int Id, std::vector const &Groups); + // name not used by any other preset, built from specified base + std::string unique_name(std::string const &Base, int IgnoreId = 0) const; + + private: + editor_track_preset *find_(int Id); + + std::vector m_presets; + int m_nextid{1}; +}; + +extern editorTrackPresets EditorTrackPresets; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 3d1358ae..7441afdf 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10166,3 +10166,95 @@ msgstr "zmieniony, niezapisany" msgid "new, not saved" msgstr "nowy, niezapisany" + +# scenery editor: presets of the track properties + +msgid "Sleeper placement" +msgstr "Rozmieszczenie podkładów" + +msgid "Overwrites nothing: choose what it overwrites in the inspector" +msgstr "Niczego nie nadpisuje: wybierz w inspektorze, co ma nadpisywać" + +msgid "Overwrites: %s" +msgstr "Nadpisuje: %s" + +msgid "Taken from %s" +msgstr "Wzięty z %s" + +msgid "The preset \"%s\" overwrites nothing: choose what it overwrites" +msgstr "Zestaw \"%s\" niczego nie nadpisuje: wybierz, co ma nadpisywać" + +msgid "Preset \"%s\" applied on %d track, Ctrl+Z takes it back" +msgid_plural "Preset \"%s\" applied on %d tracks, Ctrl+Z takes it back" +msgstr[0] "Zestaw \"%s\" zastosowany na %d torze, Ctrl+Z go cofa" +msgstr[1] "Zestaw \"%s\" zastosowany na %d torach, Ctrl+Z go cofa" +msgstr[2] "Zestaw \"%s\" zastosowany na %d torach, Ctrl+Z go cofa" + +msgid "The tracks have the properties of the preset already" +msgstr "Tory mają już właściwości tego zestawu" + +msgid "Presets" +msgstr "Zestawy" + +msgid "No presets yet" +msgstr "Brak zestawów" + +msgid "Apply on %d track" +msgid_plural "Apply on %d tracks" +msgstr[0] "Zastosuj na %d torze" +msgstr[1] "Zastosuj na %d torach" +msgstr[2] "Zastosuj na %d torach" + +msgid "Puts the values of the preset on the selected tracks, of what it overwrites; Ctrl+Z takes them back.\nTools in the menu applies the presets too" +msgstr "Nadaje zaznaczonym torom wartości zestawu, z tego, co nadpisuje; Ctrl+Z je cofa.\nZestawy można też zastosować z menu Narzędzia" + +msgid "New from the track" +msgstr "Nowy z toru" + +msgid "New preset" +msgstr "Nowy zestaw" + +msgid "Properties of %s saved as a preset: give it a name and choose what it overwrites" +msgstr "Właściwości %s zapisane jako zestaw: nadaj mu nazwę i wybierz, co ma nadpisywać" + +msgid "Saves the properties of this track as a new preset" +msgstr "Zapisuje właściwości tego toru jako nowy zestaw" + +msgid "Select one track to save its properties as a preset" +msgstr "Zaznacz jeden tor, żeby zapisać jego właściwości jako zestaw" + +msgid "A preset keeps the properties of a track: its rails, trackbed, sleepers... to put them on other tracks,\nwith the choice of what it overwrites" +msgstr "Zestaw przechowuje właściwości toru: szyny, podsypkę, podkłady... żeby nadać je innym torom,\nz wyborem tego, co nadpisuje" + +msgid "Overwrites:" +msgstr "Nadpisuje:" + +msgid "Update from the track" +msgstr "Aktualizuj z toru" + +msgid "Preset \"%s\" takes the properties of %s now" +msgstr "Zestaw \"%s\" ma teraz właściwości %s" + +msgid "The preset takes the properties of this track in place of the ones it had; what it overwrites stays" +msgstr "Zestaw przejmuje właściwości tego toru w miejsce dotychczasowych; wybór tego, co nadpisuje, zostaje" + +msgid "Select one track to take its properties into the preset" +msgstr "Zaznacz jeden tor, żeby przenieść jego właściwości do zestawu" + +msgid "Delete preset \"%s\"?" +msgstr "Usunąć zestaw \"%s\"?" + +msgid "Track presets" +msgstr "Zestawy torów" + +msgid "No presets: save one in the inspector of a track, Look, Sleeper models, Presets" +msgstr "Brak zestawów: zapisz zestaw w inspektorze toru, Wygląd, Modele podkładów, Zestawy" + +msgid "Select the tracks to apply the preset on first" +msgstr "Najpierw zaznacz tory, na których zastosować zestaw" + +msgid "Applies the preset on %d selected track" +msgid_plural "Applies the preset on %d selected tracks" +msgstr[0] "Stosuje zestaw na %d zaznaczonym torze" +msgstr[1] "Stosuje zestaw na %d zaznaczonych torach" +msgstr[2] "Stosuje zestaw na %d zaznaczonych torach"