Files
essim/src/frontends/wx/wx_frame.cpp
François c2b1f4c4ae Extract duplicate into core; support it in the script engine + wx GUI
A script using `duplicate` failed with "unsupported command 'duplicate'"
because the clone logic was still inline in the tui command. Extract it to
core/app/edit.hpp::duplicate_module(System*, src, dst) -> {ok, error, parts,
signals}: a deep clone of a module (parts, pins with spec + nc_origin, signals
with type overrides, pin→signal wiring; no connections), refusing on an unknown
source or an already-taken destination name.

  - the tui `duplicate` command renders the result (output unchanged);
  - the script engine dispatches `duplicate` to it — the failing script now runs;
  - the wx GUI gains Edit ▸ Duplicate module… (PickModule + a name prompt).

tests/test_edit.cpp: deep clone wires to the clone's own signal (not the
source's) and preserves the type; unknown source / existing destination
refused. 412 core assertions green; tui + wx build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:04:45 +02:00

680 lines
26 KiB
C++

#include "frontends/wx/wx_frame.hpp"
#include "frontends/wx/wx_frontend.hpp"
#include "core/app/connect.hpp"
#include "core/app/edit.hpp"
#include "core/app/export.hpp"
#include "core/app/load.hpp"
#include "core/app/script.hpp"
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/persist.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signal_type.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <wx/wx.h>
#include <wx/choicdlg.h>
#include <wx/filedlg.h>
#include <wx/filename.h>
#include <wx/textdlg.h>
#include <wx/treectrl.h>
#include <algorithm>
#include <cctype>
#include <sstream>
#include <string>
#include <vector>
namespace {
enum {
ID_LOAD = wxID_HIGHEST + 1,
ID_RESTORE,
ID_RUN_SCRIPT,
ID_SAVE,
ID_EXPORT,
ID_SET_CONNECTOR_TYPE,
ID_ATTACH_BSDL,
ID_SET_SIGNAL_TYPE,
ID_CONNECT,
ID_DUPLICATE,
ID_VERIFY,
ID_QUIT,
ID_ABOUT,
};
// Core (UTF-8 std::string) -> wxString, and back for paths.
inline wxString wx(const std::string &s) { return wxString::FromUTF8(s.c_str()); }
// Natural order ("2" < "10", "A2" < "A10") so pin/part lists read intuitively.
bool natural_less(const std::string &a, const std::string &b) {
size_t i = 0, j = 0;
while (i < a.size() && j < b.size()) {
unsigned char ca = a[i], cb = b[j];
if (std::isdigit(ca) && std::isdigit(cb)) {
size_t i0 = i, j0 = j;
while (i < a.size() && std::isdigit((unsigned char)a[i])) ++i;
while (j < b.size() && std::isdigit((unsigned char)b[j])) ++j;
std::string na = a.substr(i0, i - i0), nb = b.substr(j0, j - j0);
na.erase(0, na.find_first_not_of('0')); // ignore leading zeros
nb.erase(0, nb.find_first_not_of('0'));
if (na.size() != nb.size()) return na.size() < nb.size();
if (na != nb) return na < nb;
} else {
if (ca != cb) return ca < cb;
++i; ++j;
}
}
return a.size() < b.size();
}
// " (Power)" / " (Gnd)" — only for the meaningful types; "" for Other.
wxString type_suffix(SignalType t) {
return t == SignalType::Other ? wxString()
: " (" + wxString(signal_type_name(t)) + ")";
}
// What a tree node stands for, attached to the item so a selection or a
// right-click can drive the edit operations on the right domain object.
struct NodeData : public wxTreeItemData {
enum class Kind { Other, Module, Part, Pin, Signal };
Kind kind;
Module *module = nullptr;
Part *part = nullptr;
Signal *signal = nullptr;
explicit NodeData(Kind k) : kind(k) {}
};
NodeData *node_of(wxTreeCtrl *tree, const wxTreeItemId &id) {
return id.IsOk() ? static_cast<NodeData *>(tree->GetItemData(id)) : nullptr;
}
// The part of the current selection — a Part node, or the Pin's owning part.
Part *selected_part(wxTreeCtrl *tree) {
NodeData *d = node_of(tree, tree->GetSelection());
if (d && (d->kind == NodeData::Kind::Part || d->kind == NodeData::Kind::Pin))
return d->part;
return nullptr;
}
// The signal of the current selection (and, via `mod`, its module).
Signal *selected_signal(wxTreeCtrl *tree, Module **mod) {
NodeData *d = node_of(tree, tree->GetSelection());
if (d && d->kind == NodeData::Kind::Signal) {
if (mod) *mod = d->module;
return d->signal;
}
return nullptr;
}
} // namespace
EssimFrame::EssimFrame(WxFrontend &fe)
: wxFrame(nullptr, wxID_ANY, "essim — system digital twin",
wxDefaultPosition, wxSize(960, 640)),
fe_(fe) {
auto *file = new wxMenu;
file->Append(ID_LOAD, "&Load module…\tCtrl-L");
file->Append(ID_RESTORE, "&Restore snapshot…\tCtrl-R");
file->Append(ID_RUN_SCRIPT, "&Run script…\tCtrl-U");
file->Append(ID_SAVE, "&Save snapshot…\tCtrl-S");
file->AppendSeparator();
file->Append(ID_EXPORT, "&Export connections…\tCtrl-E");
file->AppendSeparator();
file->Append(ID_QUIT, "&Quit\tCtrl-Q");
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");
edit->AppendSeparator();
edit->Append(ID_DUPLICATE, "&Duplicate module…\tCtrl-D");
auto *sysm = new wxMenu;
sysm->Append(ID_VERIFY, "&Verify\tCtrl-K");
auto *help = new wxMenu;
help->Append(ID_ABOUT, "&About");
auto *bar = new wxMenuBar;
bar->Append(file, "&File");
bar->Append(edit, "&Edit");
bar->Append(sysm, "&System");
bar->Append(help, "&Help");
SetMenuBar(bar);
CreateStatusBar();
SetStatusText("essim — wx frontend");
auto *panel = new wxPanel(this);
tree_ = new wxTreeCtrl(panel, wxID_ANY);
overview_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
log_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
wxFont mono(wxFontInfo().Family(wxFONTFAMILY_TELETYPE));
overview_->SetFont(mono);
log_->SetFont(mono);
// Cap each control's minimum so its *content* can't inflate the layout's
// minimum size: on GTK a full tree/text reports a large natural size, which
// would otherwise eat all the vertical space and freeze the log at its
// minimum (it stopped resizing once a script populated the tree). With a
// modest min, the sizer proportions govern and content scrolls inside.
tree_->SetMinSize(wxSize(260, 120));
overview_->SetMinSize(wxSize(260, 120));
log_->SetMinSize(wxSize(420, 90));
auto *top = new wxBoxSizer(wxHORIZONTAL);
top->Add(tree_, 1, wxEXPAND | wxALL, 4);
top->Add(overview_, 1, wxEXPAND | wxALL, 4);
auto *root = new wxBoxSizer(wxVERTICAL);
root->Add(top, 2, wxEXPAND);
root->Add(new wxStaticText(panel, wxID_ANY, " Log"), 0, wxLEFT | wxTOP, 6);
root->Add(log_, 1, wxEXPAND | wxALL, 4);
panel->SetSizer(root);
// Drive the panel from a frame sizer so it fills the client area and
// re-lays-out on every resize (the implicit single-child fill is not
// reliable here — without this the log keeps its size when the window grows).
auto *frame_sizer = new wxBoxSizer(wxVERTICAL);
frame_sizer->Add(panel, 1, wxEXPAND);
SetSizer(frame_sizer);
Bind(wxEVT_MENU, &EssimFrame::OnLoad, this, ID_LOAD);
Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE);
Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE);
Bind(wxEVT_MENU, &EssimFrame::OnRunScript, this, ID_RUN_SCRIPT);
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::OnDuplicateModule, this, ID_DUPLICATE);
Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY);
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
tree_->Bind(wxEVT_TREE_ITEM_MENU, &EssimFrame::OnTreeContextMenu, this);
RebuildModelView();
}
void EssimFrame::Log(const wxString &line) {
log_->AppendText(line + "\n");
}
void EssimFrame::RebuildModelView() {
System *sys = fe_.system();
tree_->DeleteAllItems();
wxTreeItemId root = tree_->AddRoot("System");
int n_mods = 0, n_parts = 0, n_sigs = 0;
if (sys) {
std::vector<std::string> mods;
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
std::sort(mods.begin(), mods.end());
n_mods = (int)mods.size();
for (const auto &mname : mods) {
Module *m = sys->modules()->get(mname);
int mp = (int)m->size();
int ms = (int)m->signals->size();
n_parts += mp;
n_sigs += ms;
wxTreeItemId mid = tree_->AppendItem(
root, wx(mname) + wxString::Format(" — %d part(s), %d signal(s)",
mp, ms));
{
auto *d = new NodeData(NodeData::Kind::Module);
d->module = m;
tree_->SetItemData(mid, d);
}
// Parts → pins (each pin shows the signal it is wired to, or NC).
std::vector<std::string> parts;
for (auto &pkv : *m) parts.push_back(pkv.first);
std::sort(parts.begin(), parts.end(), natural_less);
for (const auto &pname : parts) {
Part *p = m->get(pname);
wxString label = wx(pname)
+ wxString::Format(" (%d pin(s))", (int)p->size());
if (!p->connector_type.empty())
label += " [" + wx(p->connector_type) + "]";
wxTreeItemId pid = tree_->AppendItem(mid, label);
{
auto *d = new NodeData(NodeData::Kind::Part);
d->module = m;
d->part = p;
tree_->SetItemData(pid, d);
}
std::vector<std::string> pins;
for (auto &nkv : *p) pins.push_back(nkv.first);
std::sort(pins.begin(), pins.end(), natural_less);
for (const auto &pinname : pins) {
Pin *pin = p->get(pinname);
wxString pl = wx(pinname) + " -> ";
if (Signal *s = pin->signal()) {
pl += wx(s->name) + type_suffix(s->type);
} else {
pl += "(NC";
if (pin->nc_origin == NcOrigin::ImportedUnconnected)
pl += ", imported";
else if (pin->nc_origin == NcOrigin::DroppedSingleton)
pl += ", dropped";
pl += ")";
}
wxTreeItemId nid = tree_->AppendItem(pid, pl);
auto *d = new NodeData(NodeData::Kind::Pin);
d->module = m;
d->part = p;
tree_->SetItemData(nid, d);
}
}
// Signals branch (the per-module net view: type + fan-out).
if (ms > 0) {
wxTreeItemId sid =
tree_->AppendItem(mid, wxString::Format("Signals (%d)", ms));
std::vector<std::string> sigs;
for (auto &skv : *m->signals) sigs.push_back(skv.first);
std::sort(sigs.begin(), sigs.end(), natural_less);
for (const auto &sname : sigs) {
Signal *s = m->signals->get(sname);
wxTreeItemId nid = tree_->AppendItem(
sid, wx(sname) + type_suffix(s->type)
+ wxString::Format(" — %d pin(s)", (int)s->size()));
auto *d = new NodeData(NodeData::Kind::Signal);
d->module = m;
d->signal = s;
tree_->SetItemData(nid, d);
}
}
tree_->Expand(mid); // parts + Signals visible; pins/nets on demand
}
}
tree_->Expand(root);
int n_conn = sys ? (int)sys->connections()->size() : 0;
wxString ov;
ov << "Modules: " << n_mods << "\n"
<< "Parts: " << n_parts << "\n"
<< "Signals: " << n_sigs << "\n"
<< "Connections: " << n_conn << "\n";
if (sys) {
app::VerifyReport r = app::verify(sys);
ov << "\nHealth (verify):\n"
<< wxString::Format(" pin-role mismatches: %d / %d typed pin(s)\n",
(int)r.role_mismatches.size(), r.typed_pins)
<< wxString::Format(" net inconsistencies: %d / %d bridged net(s)\n",
(int)r.net_inconsistencies.size(), r.bridged_nets)
<< wxString::Format(" orphan pins: %d (%d imported, %d dropped)\n",
r.orphan_total(), r.orphan_imported, r.orphan_dropped)
<< wxString::Format(" model anomalies: %d\n", r.model_total());
}
overview_->SetValue(ov);
}
void EssimFrame::OnLoad(wxCommandEvent &) {
wxFileDialog dlg(this, "Load a netlist / pinout file", "", "",
"All files (*.*)|*.*", wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK) return;
const wxString path = dlg.GetPath();
wxString modname = wxGetTextFromUser("Module name:", "Load module",
wxFileName(path).GetName(), this);
if (modname.empty()) return;
static const wxString kinds[] = {"mentor", "altium", "ods"};
int ki = wxGetSingleChoiceIndex("Import type:", "Load module",
WXSIZEOF(kinds), kinds, this);
if (ki < 0) return;
ImportType type;
app::import_type_from_name(kinds[ki].ToStdString(), type); // choice is valid
app::LoadResult r = app::load_module(
fe_.system(), modname.utf8_string(), path.utf8_string(), type);
if (!r.ok) {
Log("load failed: " + wx(r.error));
wxMessageBox(wx(r.error), "Load failed", wxOK | wxICON_ERROR, this);
return;
}
Log(wxString::Format(
"loaded '%s' from %s — %d part(s), %d signal(s)"
" (dropped %d; types: %d power / %d gnd / %d suspect)",
modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd,
r.kept_other));
RebuildModelView();
}
void EssimFrame::OnRestore(wxCommandEvent &) {
wxFileDialog dlg(this, "Restore a system snapshot", "", "",
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK) return;
std::string err;
System *fresh = restore_system(dlg.GetPath().utf8_string(), err);
if (!fresh) {
Log("restore failed: " + wx(err));
wxMessageBox(wx(err), "Restore failed", wxOK | wxICON_ERROR, this);
return;
}
fe_.set_system(fresh);
Log("restored from " + dlg.GetPath());
RebuildModelView();
}
void EssimFrame::OnRunScript(wxCommandEvent &) {
wxFileDialog dlg(this, "Run an essim script", "", "",
"essim scripts (*.essim)|*.essim|All files (*.*)|*.*",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK) return;
fe_.ensure_system();
std::ostringstream out;
app::ScriptResult r =
app::run_script(fe_.system_ptr(), dlg.GetPath().utf8_string(), out);
if (!r.ok) {
Log("run script: " + wx(r.error));
wxMessageBox(wx(r.error), "Run script", wxOK | wxICON_ERROR, this);
return;
}
// Echo each line of the script's output into the log pane.
std::istringstream lines(out.str());
std::string line;
while (std::getline(lines, line)) Log(wx(line));
Log(wxString::Format("source: %s (%d line(s), %d error(s))",
dlg.GetPath(), r.lines, r.errors));
RebuildModelView();
}
void EssimFrame::OnSave(wxCommandEvent &) {
wxFileDialog dlg(this, "Save system snapshot", "", "system.essim",
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dlg.ShowModal() != wxID_OK) return;
std::string err;
if (save_system(fe_.system(), dlg.GetPath().utf8_string(), err)) {
Log("saved to " + dlg.GetPath());
} else {
Log("save failed: " + wx(err));
wxMessageBox(wx(err), "Save failed", wxOK | wxICON_ERROR, this);
}
}
void EssimFrame::OnExport(wxCommandEvent &) {
wxFileDialog dlg(this, "Export connections", "", "connections.csv",
"CSV (*.csv)|*.csv|OpenDocument sheet (*.ods)|*.ods",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dlg.ShowModal() != wxID_OK) return;
const std::string path = dlg.GetPath().utf8_string();
app::ExportFormat fmt;
if (!app::export_format_from_path(path, fmt)) {
wxMessageBox("Unknown export extension (use .csv or .ods).",
"Export failed", wxOK | wxICON_ERROR, this);
return;
}
app::ExportResult r = app::export_connections(fe_.system(), path, fmt);
if (r.ok) {
Log(wxString::Format("exported %d row(s) to %s", r.rows, dlg.GetPath()));
} else {
Log("export failed: " + wx(r.error));
wxMessageBox(wx(r.error), "Export failed", wxOK | wxICON_ERROR, this);
}
}
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<std::string> mods;
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
std::sort(mods.begin(), mods.end());
wxArrayString choices;
for (const auto &m : mods) choices.Add(wx(m));
int mi = wxGetSingleChoiceIndex("Module:", caption, choices, this);
if (mi < 0) return nullptr;
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);
return nullptr;
}
std::vector<std::string> parts;
for (auto &pkv : *m) parts.push_back(pkv.first);
std::sort(parts.begin(), parts.end());
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 = selected_part(tree_);
if (!p) p = PickPart();
if (!p) return;
wxTextEntryDialog dlg(this, "Connector type (empty = none):",
"Set connector type", wx(p->connector_type));
if (dlg.ShowModal() != wxID_OK) return;
const std::string kind = dlg.GetValue().utf8_string();
app::SetConnectorTypeResult r = app::set_connector_type(p, kind);
if (!r.ok) {
Log("set-connector-type refused: " + wx(r.error));
wxMessageBox(wx(r.error), "Refused", wxOK | wxICON_ERROR, this);
return;
}
wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name);
Log(who + ": connector_type = " + (kind.empty() ? wxString("(none)") : wx(kind)));
if (r.materialised > 0)
Log(wxString::Format(" added %d NC pin(s) from the connector layout",
r.materialised));
RebuildModelView();
}
void EssimFrame::OnAttachBsdl(wxCommandEvent &) {
Part *p = selected_part(tree_);
if (!p) p = PickPart();
if (!p) return;
wxFileDialog dlg(this, "Attach a BSDL model", "", "",
"BSDL files (*.bsd)|*.bsd|All files (*.*)|*.*",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK) return;
app::AttachBsdlResult r = app::attach_bsdl(p, dlg.GetPath().utf8_string());
if (!r.ok) {
Log("attach-bsdl: " + wx(r.error));
wxMessageBox(wx(r.error), "Attach BSDL failed", wxOK | wxICON_ERROR, this);
return;
}
wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name);
wxString tail = r.unbound ? wxString::Format(", %d unbound", r.unbound)
: wxString();
Log(wxString::Format("%s: attached BSDL '%s' — %d/%d ports bound%s",
who, wx(r.entity), r.bound, r.ports_total, tail));
RebuildModelView();
}
void EssimFrame::OnConnect(wxCommandEvent &) {
Part *p1 = selected_part(tree_);
if (!p1) p1 = PickPart("Connect — first part");
if (!p1) return;
Part *p2 = PickPart("Connect — second part");
if (!p2) return;
if (p1 == p2) {
wxMessageBox("Pick two different parts.", "Connect",
wxOK | wxICON_INFORMATION, this);
return;
}
// m1/m2 are the parts' parent modules — connect_parts needs them for the
// Connection name and ownership.
app::ConnectResult r =
app::connect_parts(fe_.system(), p1->prnt, p1, p2->prnt, p2);
if (r.refused) {
Log("connect refused: " + wx(r.error));
wxMessageBox(wx(r.error), "Connect refused", wxOK | wxICON_ERROR, this);
return;
}
if (!r.identity_info.empty()) {
Log("connect: " + wx(r.identity_info));
if (r.nc_added > 0)
Log(wxString::Format("connect: added %d NC pin(s) so both sides match",
r.nc_added));
}
if (r.ok) {
Log(wxString::Format("connected: %s via %s (%d wires)",
wx(r.connection_name), wx(r.transform_name), r.wires));
} else {
Log("connect failed: " + wx(r.error));
wxMessageBox(wx(r.error), "Connect failed", wxOK | wxICON_ERROR, this);
}
RebuildModelView();
}
void EssimFrame::OnSetSignalType(wxCommandEvent &) {
Module *m = nullptr;
Signal *sig = selected_signal(tree_, &m);
if (!sig) {
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<std::string> sigs;
for (auto &skv : *m->signals) sigs.push_back(skv.first);
std::sort(sigs.begin(), sigs.end(), natural_less);
wxArrayString schoices;
for (const auto &s : sigs) schoices.Add(wx(s));
int si = wxGetSingleChoiceIndex("Signal:", "Set signal type", schoices, this);
if (si < 0) return;
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::OnDuplicateModule(wxCommandEvent &) {
Module *m = PickModule("Duplicate module");
if (!m) return;
const std::string src = m->name; // m may move in the table after the add
wxTextEntryDialog dlg(this, "New module name:", "Duplicate module",
wx(src) + "_copy");
if (dlg.ShowModal() != wxID_OK) return;
const std::string dst = dlg.GetValue().utf8_string();
if (dst.empty()) return;
app::DuplicateResult r = app::duplicate_module(fe_.system(), src, dst);
if (!r.ok) {
Log(wx(r.error));
wxMessageBox(wx(r.error), "Duplicate module", wxOK | wxICON_ERROR, this);
return;
}
Log(wx("duplicate: '" + src + "' → '" + dst + "' ("
+ std::to_string(r.parts) + " part(s), "
+ std::to_string(r.signals) + " signal(s))"));
RebuildModelView();
}
void EssimFrame::OnVerify(wxCommandEvent &) {
app::VerifyReport r = app::verify(fe_.system());
Log("verify:");
Log(wxString::Format(" %d pin-role mismatch(es) over %d typed pin(s)",
(int)r.role_mismatches.size(), r.typed_pins));
for (const auto &m : r.role_mismatches)
Log(wx(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
+ signal_type_name(m.expected) + ", got "
+ signal_type_name(m.actual)));
Log(wxString::Format(" %d inconsistent net(s) over %d bridged net(s)",
(int)r.net_inconsistencies.size(), r.bridged_nets));
Log(wxString::Format(" %d orphan pin(s) (%d imported, %d dropped)",
r.orphan_total(), r.orphan_imported, r.orphan_dropped));
auto log_anoms = [this](const std::vector<Anomaly> &v, const char *tail) {
Log(wxString::Format(" %d %s", (int)v.size(), tail));
for (const auto &a : v)
Log(wx(std::string(" [") + anomaly_kind_name(a.kind) + "] "
+ a.message));
};
log_anoms(r.pin_anomalies, "model-driven pin anomaly(ies)");
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
log_anoms(r.conflict_anomalies, "source-conflict(s)");
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
RebuildModelView();
}
void EssimFrame::OnQuit(wxCommandEvent &) { Close(true); }
void EssimFrame::OnAbout(wxCommandEvent &) {
wxMessageBox("essim — system digital twin\n\n"
"wxWidgets frontend over essim_core.",
"About essim", wxOK | wxICON_INFORMATION, this);
}
void EssimFrame::OnTreeContextMenu(wxTreeEvent &ev) {
wxTreeItemId id = ev.GetItem();
if (id.IsOk()) tree_->SelectItem(id); // the edit handlers read the selection
NodeData *d = node_of(tree_, id);
if (!d) return;
// Reuse the menu IDs so these route to the same handlers, which now act on
// the (just-selected) tree item.
wxMenu menu;
if (d->kind == NodeData::Kind::Part || d->kind == NodeData::Kind::Pin) {
menu.Append(ID_SET_CONNECTOR_TYPE, "Set connector type…");
menu.Append(ID_ATTACH_BSDL, "Attach BSDL…");
menu.Append(ID_CONNECT, "Connect to…");
} else if (d->kind == NodeData::Kind::Signal) {
menu.Append(ID_SET_SIGNAL_TYPE, "Set signal type…");
}
if (menu.GetMenuItemCount() > 0) PopupMenu(&menu);
}