#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 #include #include #include #include #include #include #include #include #include #include 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(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 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 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 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 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 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 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 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 &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); }