From 19dbec967240301df460597f9915a413e2b078d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 21:27:44 +0200 Subject: [PATCH] Extract set-signal-type into core; add it to the wx GUI. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth editing op into the wx frontend. Extract the type-name parse + apply into core/app/edit.hpp::set_signal_type(Signal*, name) -> {ok, error, type}, failing without mutation on an unrecognised name. The interactive sigtype modal keeps its own SignalType-cycling path (different interaction, trivial mutation). The TUI `set-signal-type` command now renders that result (output unchanged). The wx GUI gains Edit ▸ Set signal type…: a shared PickModule() helper (PickPart now builds on it) + inline signal choice + a power/gnd/other dropdown, then the core op, logged as "module/signal: signal type = …" and reflected. tests/test_edit.cpp: name parsed and applied; unknown name refused without mutation. 387 core assertions green; tui + wx build clean. Co-Authored-By: Claude Opus 4.8 --- src/core/app/edit.cpp | 21 ++++++++++++ src/core/app/edit.hpp | 14 ++++++++ src/frontends/tui/commands.cpp | 10 ++---- src/frontends/wx/wx_frame.cpp | 58 ++++++++++++++++++++++++++++------ src/frontends/wx/wx_frame.hpp | 10 +++--- tests/test_edit.cpp | 19 +++++++++++ 6 files changed, 112 insertions(+), 20 deletions(-) diff --git a/src/core/app/edit.cpp b/src/core/app/edit.cpp index 65f4c2b..45507ca 100644 --- a/src/core/app/edit.cpp +++ b/src/core/app/edit.cpp @@ -3,6 +3,7 @@ #include "core/domain/bsdl_model.hpp" #include "core/domain/parts.hpp" #include "core/domain/pin_model.hpp" +#include "core/domain/signals.hpp" #include "core/domain/transform_vpx.hpp" // ValidatePartForKind namespace app { @@ -54,4 +55,24 @@ AttachBsdlResult attach_bsdl(Part *part, const std::string &path) return r; } +SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name) +{ + SetSignalTypeResult r; + if (!sig) { + r.error = "no signal"; + return r; + } + + SignalType t; + if (!signal_type_from_name(type_name, t)) { + r.error = "type must be one of: power, gnd, other (got: " + type_name + ")"; + return r; + } + + sig->type = t; + r.type = t; + r.ok = true; + return r; +} + } // namespace app diff --git a/src/core/app/edit.hpp b/src/core/app/edit.hpp index 9dc52f8..d30a3b3 100644 --- a/src/core/app/edit.hpp +++ b/src/core/app/edit.hpp @@ -1,9 +1,12 @@ #ifndef _APP_EDIT_HPP_ #define _APP_EDIT_HPP_ +#include "core/domain/signal_type.hpp" // SignalType + #include class Part; +class Signal; // Application layer: UI-independent part-editing operations any frontend can // call. No console, no dialogs, no FTXUI — Part in, result struct out. @@ -38,6 +41,17 @@ struct AttachBsdlResult { // when the file cannot be parsed. AttachBsdlResult attach_bsdl(Part *part, const std::string &path); +// Outcome of overriding a signal's type from a user-supplied name. +struct SetSignalTypeResult { + bool ok = false; + std::string error; ///< set when the name isn't power/gnd/other + SignalType type = SignalType::Other; ///< the resolved type (for rendering) +}; + +// Set `sig`'s type from `type_name` (power | gnd | other, case-insensitive). +// Fails (ok=false, error set, no mutation) on an unrecognised name. +SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name); + } // namespace app #endif // _APP_EDIT_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 31e4537..9789d20 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -334,14 +334,10 @@ void Tui::RegisterCommands() { catch (const std::exception &) { Print("unknown signal: " + mod->name + "/" + args[1]); return; } - SignalType t; - if (!signal_type_from_name(args[2], t)) { - Print("type must be one of: power, gnd, other (got: " + args[2] + ")"); - return; - } - sig->type = t; + app::SetSignalTypeResult r = app::set_signal_type(sig, args[2]); + if (!r.ok) { Print(r.error); return; } Print(mod->name + "/" + sig->name + ": signal type = " - + signal_type_name(t)); + + signal_type_name(r.type)); }, /*prompt_for_missing=*/ true, "override the auto-detected signal type (power | gnd | other)", diff --git a/src/frontends/wx/wx_frame.cpp b/src/frontends/wx/wx_frame.cpp index 732ef1a..ccc4654 100644 --- a/src/frontends/wx/wx_frame.cpp +++ b/src/frontends/wx/wx_frame.cpp @@ -35,6 +35,7 @@ enum { ID_EXPORT, ID_SET_CONNECTOR_TYPE, ID_ATTACH_BSDL, + ID_SET_SIGNAL_TYPE, ID_CONNECT, ID_VERIFY, ID_QUIT, @@ -61,6 +62,7 @@ EssimFrame::EssimFrame(WxFrontend &fe) auto *edit = new wxMenu; edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T"); edit->Append(ID_ATTACH_BSDL, "Attach &BSDL…\tCtrl-B"); + edit->Append(ID_SET_SIGNAL_TYPE, "Set &signal type…\tCtrl-G"); edit->AppendSeparator(); edit->Append(ID_CONNECT, "C&onnect parts…\tCtrl-O"); @@ -107,6 +109,7 @@ EssimFrame::EssimFrame(WxFrontend &fe) Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT); Bind(wxEVT_MENU, &EssimFrame::OnSetConnectorType, this, ID_SET_CONNECTOR_TYPE); Bind(wxEVT_MENU, &EssimFrame::OnAttachBsdl, this, ID_ATTACH_BSDL); + Bind(wxEVT_MENU, &EssimFrame::OnSetSignalType, this, ID_SET_SIGNAL_TYPE); Bind(wxEVT_MENU, &EssimFrame::OnConnect, this, ID_CONNECT); Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY); Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT); @@ -261,23 +264,26 @@ void EssimFrame::OnExport(wxCommandEvent &) { } } -Part *EssimFrame::PickPart(const wxString &caption) { +Module *EssimFrame::PickModule(const wxString &caption) { System *sys = fe_.system(); if (!sys || sys->modules()->size() == 0) { wxMessageBox("No modules loaded.", caption, wxOK | wxICON_INFORMATION, this); return nullptr; } - std::vector mods; for (auto &mkv : *sys->modules()) mods.push_back(mkv.first); std::sort(mods.begin(), mods.end()); - wxArrayString mchoices; - for (const auto &m : mods) mchoices.Add(wx(m)); - int mi = wxGetSingleChoiceIndex("Module:", caption, mchoices, this); + wxArrayString choices; + for (const auto &m : mods) choices.Add(wx(m)); + int mi = wxGetSingleChoiceIndex("Module:", caption, choices, this); if (mi < 0) return nullptr; - Module *m = sys->modules()->get(mods[mi]); + return sys->modules()->get(mods[mi]); +} +Part *EssimFrame::PickPart(const wxString &caption) { + Module *m = PickModule(caption); + if (!m) return nullptr; if (m->size() == 0) { wxMessageBox("That module has no parts.", caption, wxOK | wxICON_INFORMATION, this); @@ -286,13 +292,14 @@ Part *EssimFrame::PickPart(const wxString &caption) { std::vector parts; for (auto &pkv : *m) parts.push_back(pkv.first); std::sort(parts.begin(), parts.end()); - wxArrayString pchoices; - for (const auto &p : parts) pchoices.Add(wx(p)); - int pi = wxGetSingleChoiceIndex("Part:", caption, pchoices, this); + wxArrayString choices; + for (const auto &p : parts) choices.Add(wx(p)); + int pi = wxGetSingleChoiceIndex("Part:", caption, choices, this); if (pi < 0) return nullptr; return m->get(parts[pi]); } + void EssimFrame::OnSetConnectorType(wxCommandEvent &) { Part *p = PickPart(); if (!p) return; @@ -378,6 +385,39 @@ void EssimFrame::OnConnect(wxCommandEvent &) { RebuildModelView(); } +void EssimFrame::OnSetSignalType(wxCommandEvent &) { + Module *m = PickModule("Set signal type"); + if (!m) return; + if (m->signals->size() == 0) { + wxMessageBox("That module has no signals.", "Set signal type", + wxOK | wxICON_INFORMATION, this); + return; + } + std::vector sigs; + for (auto &skv : *m->signals) sigs.push_back(skv.first); + std::sort(sigs.begin(), sigs.end()); + wxArrayString schoices; + for (const auto &s : sigs) schoices.Add(wx(s)); + int si = wxGetSingleChoiceIndex("Signal:", "Set signal type", schoices, this); + if (si < 0) return; + Signal *sig = m->signals->get(sigs[si]); + + static const wxString types[] = {"power", "gnd", "other"}; + int ti = wxGetSingleChoiceIndex("Type:", "Set signal type", + WXSIZEOF(types), types, this); + if (ti < 0) return; + + app::SetSignalTypeResult r = app::set_signal_type(sig, types[ti].ToStdString()); + if (!r.ok) { + Log(wx(r.error)); + wxMessageBox(wx(r.error), "Set signal type", wxOK | wxICON_ERROR, this); + return; + } + Log(wxString::Format("%s/%s: signal type = %s", wx(m->name), wx(sig->name), + wx(signal_type_name(r.type)))); + RebuildModelView(); +} + void EssimFrame::OnVerify(wxCommandEvent &) { app::VerifyReport r = app::verify(fe_.system()); diff --git a/src/frontends/wx/wx_frame.hpp b/src/frontends/wx/wx_frame.hpp index 4c9f14e..571ea13 100644 --- a/src/frontends/wx/wx_frame.hpp +++ b/src/frontends/wx/wx_frame.hpp @@ -25,14 +25,16 @@ private: void OnSetConnectorType(wxCommandEvent &); void OnAttachBsdl(wxCommandEvent &); void OnConnect(wxCommandEvent &); + void OnSetSignalType(wxCommandEvent &); void OnVerify(wxCommandEvent &); void OnQuit(wxCommandEvent &); void OnAbout(wxCommandEvent &); - // Prompt the user to pick a module then a part from the current System. - // `caption` titles the dialogs (e.g. to distinguish two picks). Returns - // nullptr if there is nothing to pick or the user cancels. - class Part *PickPart(const wxString &caption = "Select part"); + // Modal pickers over the current System. `caption` titles the dialogs (e.g. + // to distinguish two picks). Each returns nullptr if there is nothing to + // pick or the user cancels. + class Module *PickModule(const wxString &caption); + class Part *PickPart(const wxString &caption = "Select part"); void RebuildModelView(); ///< refresh tree + overview from the System void Log(const wxString &line); ///< append a line to the log pane diff --git a/tests/test_edit.cpp b/tests/test_edit.cpp index 8007436..150b9bf 100644 --- a/tests/test_edit.cpp +++ b/tests/test_edit.cpp @@ -3,6 +3,8 @@ #include "core/app/edit.hpp" #include "core/domain/parts.hpp" #include "core/domain/pins.hpp" +#include "core/domain/signal_type.hpp" +#include "core/domain/signals.hpp" // app::set_connector_type is pure core: validate the kind, tag the part and // apply the connector model. No Print/dialog/FTXUI. @@ -54,3 +56,20 @@ TEST_CASE("attach_bsdl on a null part fails cleanly") { CHECK_FALSE(r.ok); CHECK_FALSE(r.error.empty()); } + +TEST_CASE("set_signal_type parses the name and sets the type") { + Signal s("NET"); + app::SetSignalTypeResult r = app::set_signal_type(&s, "power"); + CHECK(r.ok); + CHECK(r.type == SignalType::Power); + CHECK(s.type == SignalType::Power); +} + +TEST_CASE("set_signal_type rejects an unknown name without mutating") { + Signal s("NET"); + s.type = SignalType::Other; + app::SetSignalTypeResult r = app::set_signal_type(&s, "bogus"); + CHECK_FALSE(r.ok); + CHECK(r.error.find("power, gnd, other") != std::string::npos); + CHECK(s.type == SignalType::Other); // unchanged +}