From da18883558afe0a57e7f55de36f5c760694920ba Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:47:26 +0200 Subject: [PATCH] feat(editor): what the editor refuses shows by the mouse cursor An error put in the status bar also shows for 4 s by the cursor, and the refusals the tools kept in their panels (a curve which can't be laid, a switch which doesn't fit, a signal which can't be moved, a drive without the variant) go there as well. --- application/editormode.cpp | 6 ++++++ application/editormode.h | 2 ++ application/editormoderoute.cpp | 8 ++++++++ application/editormodesignal.cpp | 12 ++++++------ application/editormodestraights.cpp | 7 ++++--- application/editormodeswitchdrive.cpp | 12 ++++++------ application/editormodeswitchplace.cpp | 24 ++++++++++++------------ application/editoruilayer.cpp | 23 +++++++++++++++++++++++ application/editoruilayer.h | 3 +++ 9 files changed, 70 insertions(+), 27 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 8155f186..1282cf12 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -973,6 +973,12 @@ void editor_mode::undo_last() } } +std::string editor_mode::refused(std::string Text) +{ + ui()->set_status(Text, true); + return Text; +} + void editor_mode::drop_step(std::size_t const Step) { if (Step == 0 || m_history.size() != Step) diff --git a/application/editormode.h b/application/editormode.h index 69c0bfa5..7f60287f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -175,6 +175,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri // a single step of the history, see undo_last() void undo_one(); void drop_step(std::size_t const Step); + std::string refused(std::string Text); void redo_one(); // the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush, // or what a frame did; the next change is recorded from now on @@ -955,6 +956,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri m_route.result = alignment::compute(m_route.design); } void route_apply(); + void route_lay(); bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last); void route_recommend(alignment::vertex &Vertex) const; void draw_route_overlay() const; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index bfc1771b..9fd4f5b5 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -573,6 +573,14 @@ bool editor_mode::route_from_line(TTrack &Track) } void editor_mode::route_apply() +{ + m_route.error.clear(); + route_lay(); + if (false == m_route.error.empty()) + refused(m_route.error); +} + +void editor_mode::route_lay() { auto &route{m_route}; route.status.clear(); diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 2e3928a8..5366b5fa 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -586,7 +586,7 @@ void editor_mode::signal_release() return; if (false == tool.moved_valid) { - tool.status = STR_C("Drop the signal by a track"); + tool.status = refused(STR_C("Drop the signal by a track")); return; } tool.moved_valid = false; @@ -600,7 +600,7 @@ bool editor_mode::signal_move_to(signal_standing const &Standing, signal_spot co auto const instance{Standing.include.instance}; if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) { - tool.status = STR_C("This signal is in a file the editor doesn't change, it can't be moved"); + tool.status = refused(STR_C("This signal is in a file the editor doesn't change, it can't be moved")); return false; } auto const &included{scene::Layers.instance(instance)}; @@ -659,7 +659,7 @@ bool editor_mode::signal_flip(scene::instance_handle const Instance) signal_spot spot; if (standing == nullptr || false == signal_standing_spot(*standing, spot)) { - m_signal.status = STR_C("No track by the signal to take it across"); + m_signal.status = refused(STR_C("No track by the signal to take it across")); return false; } auto const across{plan_of(spot.position) - plan_of(spot.axis)}; @@ -675,7 +675,7 @@ bool editor_mode::signal_turn(scene::instance_handle const Instance) signal_spot spot; if (standing == nullptr || false == signal_standing_spot(*standing, spot)) { - m_signal.status = STR_C("No track by the signal to turn it to"); + m_signal.status = refused(STR_C("No track by the signal to turn it to")); return false; } auto const *item{signal_template_of(standing->include.file)}; @@ -730,7 +730,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T std::string name{tool.name}; if (name.empty() || name.find_first_of(" \t\";") != std::string::npos) { - tool.status = STR_C("Give the signal a name without spaces first"); + tool.status = refused(STR_C("Give the signal a name without spaces first")); return false; } signal_refresh(true); @@ -753,7 +753,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T std::string error; if (place_templates({item}, error) == 0) { - tool.status = STR_C("The signal couldn't be placed: ") + error; + tool.status = refused(STR_C("The signal couldn't be placed: ") + error); return false; } auto const instance{m_history.back().instances.front()}; diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 13a43cc9..265a0e83 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -570,7 +570,7 @@ void editor_mode::straights_apply(std::vector const &Lin return false == finite(End.first) || false == finite(End.second) || false == (glm::distance(End.first, End.second) >= 0.1 && glm::distance(End.first, End.second) <= 100000.0); })) { - state.status = STR_C("The ends have to be numbers, from 0.1 m to 100 km apart"); + state.status = refused(STR_C("The ends have to be numbers, from 0.1 m to 100 km apart")); return; } auto const tracks{editor_track::straight_affected(Lines)}; @@ -627,6 +627,7 @@ void editor_mode::render_straight_gizmo() auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; if (false == tracks_editable(tracks, state.status)) { + refused(state.status); state.fitting = false; return; } @@ -947,7 +948,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); undo_split(); - state.status = m_route.error; + state.status = refused(m_route.error); return; } m_route.design.start_reserve = 0.0; @@ -969,7 +970,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); undo_split(); - state.status = m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front(); + state.status = refused(m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front()); return; } auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; diff --git a/application/editormodeswitchdrive.cpp b/application/editormodeswitchdrive.cpp index f692c2fa..9b19c419 100644 --- a/application/editormodeswitchdrive.cpp +++ b/application/editormodeswitchdrive.cpp @@ -193,7 +193,7 @@ bool editor_mode::place_switch_drive(TTrack &Switch) item.file = family.files[index]; if (item.file.empty()) { - tool.status = missing_drive_variant(tool.drive, spot->rightswitch, driveright); + tool.status = refused(missing_drive_variant(tool.drive, spot->rightswitch, driveright)); return false; } item.location = spot->location + glm::dvec3{0.0, family.heights[index], 0.0}; @@ -206,7 +206,7 @@ bool editor_mode::place_switch_drive(TTrack &Switch) std::string error; if (place_templates({item}, error) == 0) { - tool.status = STR_C("The drive couldn't be placed: ") + error; + tool.status = refused(STR_C("The drive couldn't be placed: ") + error); return false; } tool.status = format(STR_C("Switch %s with the drive %s: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), item.file.c_str(), Switch.name().c_str(), Switch.name().c_str()); @@ -253,7 +253,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co auto const present{find_switch_drive(Switch)}; if (false == present.has_value()) { - tool.status = format(STR_C("No drive of the switch %s among the described drives"), Switch.name().c_str()); + tool.status = refused(format(STR_C("No drive of the switch %s among the described drives"), Switch.name().c_str())); return false; } auto const &found{present->standing}; @@ -263,7 +263,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co item.file = tool.drives[family].files[index]; if (item.file.empty()) { - tool.status = Flip ? format(STR_C("The drive %s has no variant with the drive on the other side"), tool.drives[family].name.c_str()) : missing_drive_variant(family, (index & 2) != 0, (index & 1) != 0); + tool.status = refused(Flip ? format(STR_C("The drive %s has no variant with the drive on the other side"), tool.drives[family].name.c_str()) : missing_drive_variant(family, (index & 2) != 0, (index & 1) != 0)); return false; } if (drive_file(item.file) == found.file) @@ -276,7 +276,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co item.tilt = {angle_value(found.values, 5), angle_value(found.values, 6)}; if (remove_templates({found.instance}) == 0) { - tool.status = STR_C("The drive of this switch can't be changed in its file"); + tool.status = refused(STR_C("The drive of this switch can't be changed in its file")); return false; } std::string error; @@ -284,7 +284,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co { scene::Layers.removed(found.instance, false); m_history.pop_back(); - tool.status = STR_C("The drive couldn't be placed: ") + error; + tool.status = refused(STR_C("The drive couldn't be placed: ") + error); return false; } join_last_steps(); diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index 1856afce..1ab64d54 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -88,7 +88,7 @@ bool editor_mode::start_switch_placement() auto *track{selected}; if (tool.templates[tool.armed].double_slip) { - tool.status = STR_C("A double slip goes only into a straight crossing another one"); + tool.status = refused(STR_C("A double slip goes only into a straight crossing another one")); return false; } editor_track::curve curve; @@ -96,12 +96,12 @@ bool editor_mode::start_switch_placement() std::string error; if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) { - tool.status = STR_C("The selected track is neither a recognized straight nor a curve"); + tool.status = refused(STR_C("The selected track is neither a recognized straight nor a curve")); return false; } if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) { - tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error; + tool.status = refused(error.empty() ? std::string{STR_C("The curve can't be followed")} : error); return false; } for (auto const &part : editor_track::chain_parts(chain)) @@ -305,7 +305,7 @@ bool editor_mode::cut_straight(editor_track::straight const &Line, double const std::string reason; if (false == tracks_editable(Line.tracks, reason)) { - m_switch.status = reason; + m_switch.status = refused(reason); ui()->set_status(reason, true); return false; } @@ -393,7 +393,7 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e auto const &shape{Shape}; if (shape.length > Line.length - 0.02) { - m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length); + m_switch.status = refused(format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length)); return false; } auto const from{switch_from(Line, shape.length, Along, Direction, Snap)}; @@ -402,7 +402,7 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e TTrack *first{nullptr}; if (false == cut_straight(Line, from, to, States, Created, Removed, &first)) { - m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track"); + m_switch.status = refused(STR_C("The switch would overlap another switch, move it onto plain track")); return false; } auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; @@ -426,7 +426,7 @@ std::vector editor_mode::create_switch_set(editor_track::switch_templa auto *link{two != nullptr ? editor_track::create_path(Style, Paths[4]) : nullptr}; if (link == nullptr) { - m_switch.status = STR_C("The switch couldn't be created from this template"); + m_switch.status = refused(STR_C("The switch couldn't be created from this template")); return {}; } editor_track::commit({one, two, link}); @@ -437,7 +437,7 @@ std::vector editor_mode::create_switch_set(editor_track::switch_templa auto *track{editor_track::create_switch(Shape, Paths, Style, Name)}; if (track == nullptr) { - m_switch.status = STR_C("The switch couldn't be created from this template"); + m_switch.status = refused(STR_C("The switch couldn't be created from this template")); return {}; } editor_track::commit({track}); @@ -974,7 +974,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv slip_plan plan; if (false == plan_double_slip(Line, Point, Shape, plan)) { - state.status = plan.problem; + state.status = refused(plan.problem); return false; } if (plan.replaces != nullptr) @@ -1041,7 +1041,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat switches[i] = simulation::Paths.find(base + "_" + static_cast('a' + i)); if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2) { - state.status = STR_C("The double slip isn't complete, it can't be replaced"); + state.status = refused(STR_C("The double slip isn't complete, it can't be replaced")); return false; } } @@ -1077,7 +1077,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat partner = i; if (partner < 0) { - state.status = STR_C("The double slip geometry isn't recognized"); + state.status = refused(STR_C("The double slip geometry isn't recognized")); return false; } auto const &a{ends[0]}; @@ -1346,7 +1346,7 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) auto const total{tool.frame.back().station}; if (shape.length > total - 0.02) { - tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length); + tool.status = refused(format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length)); return; } auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index faf4df3b..10dbe775 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -646,6 +646,7 @@ void editor_ui::render_() // strips of the unpinned docks, inside the toolbar and the status bar; the pins over the tab bars drawn by now autohide_strips(); autohide_pins(); + render_refusal(); } // updates state of UI elements @@ -1026,6 +1027,28 @@ void editor_ui::set_status(std::string const &Status, bool const Error) { m_layerspanel.status = Status; m_layerspanel.status_error = Error; + if (Error && false == Status.empty()) + { + m_refusal = Status; + m_refusaltime = ImGui::GetTime(); + } +} + +void editor_ui::render_refusal() +{ + auto const age{ImGui::GetTime() - m_refusaltime}; + if (m_refusal.empty() || age > 4.0) + return; + auto const &io{ImGui::GetIO()}; + auto const alpha{static_cast(std::clamp(4.0 - age, 0.0, 1.0))}; + auto const wrap{std::round(ImGui::GetFontSize() * 30.0f)}; + auto const size{ImGui::CalcTextSize(m_refusal.c_str(), nullptr, false, wrap)}; + ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y - 24.0f - size.y}; + at.x = std::clamp(at.x, 10.0f, std::max(10.0f, io.DisplaySize.x - size.x - 10.0f)); + at.y = std::clamp(at.y, 10.0f, std::max(10.0f, io.DisplaySize.y - size.y - 10.0f)); + auto *draw{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + draw->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(40, 0, 0, static_cast(210 * alpha)), 4.0f); + draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), at, IM_COL32(255, 150, 130, static_cast(255 * alpha)), m_refusal.c_str(), nullptr, wrap); } void editor_ui::render_rotation_controls() diff --git a/application/editoruilayer.h b/application/editoruilayer.h index b6c8a934..9fe7ee28 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -165,6 +165,7 @@ class editor_ui : public ui_layer private: // methods + void render_refusal(); void render_mode_options(nodebank_panel::edit_mode const Mode); // windows of the editor drawn by the user interface itself void render_tool_options(); @@ -208,6 +209,8 @@ class editor_ui : public ui_layer std::function m_workspacebar; std::function m_editmenu; bool m_layoutreset{false}; // the default layout is to be built again on the next frame + std::string m_refusal; + double m_refusaltime{-100.0}; bool m_tooloptionsopen{true}; bool m_inspectoropen{true}; bool m_sceneopen{true};