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>
680 lines
26 KiB
C++
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);
|
|
}
|