Add a wxWidgets GUI frontend (second frontend, proves the split).
A native wx GUI built entirely on essim_core via the Frontend interface — no
Tui reuse, no command shell. Demonstrates the core/frontends architecture by
adding a real second frontend:
- WxFrontend : public Frontend — owns the System + a console buffer; handles
boot headlessly (restore for --restore/--batch; honest note for source);
Run() boots wx without a generated main (SetInstance + wxEntryStart/
CallOnInit/OnRun) so the shared frontend_main stays in control.
- EssimFrame (wxFrame) — menu-driven window: Load (app::load_module), Restore/
Save (persist), Export (app::export_connections), Verify (app::verify),
rendered into a model tree (modules → parts), an overview + verify-health
panel, and a log. Each handler is a thin wrapper over a core/app op.
- main.cpp: construct WxFrontend, call frontend_main — same 4 lines as tui.
- CMakeLists.txt: find_package(wxWidgets) + essim_add_frontend(wx LIBS …);
select with -DESSIM_FRONTEND=wx. ESSIM_FRONTEND gains wx in its STRINGS.
Set LC_CTYPE from the environment at GUI init so wxString decodes the UTF-8 in
narrow literals (em dash, ellipsis); LC_NUMERIC stays "C". .gitignore: build*/.
Needs libwxgtk3.2-dev. Verified: builds clean (wx 3.2.9 GTK3), window opens and
renders with no wx asserts; --commands-md/--restore/--batch behave headlessly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
285
src/frontends/wx/wx_frame.cpp
Normal file
285
src/frontends/wx/wx_frame.cpp
Normal file
@@ -0,0 +1,285 @@
|
||||
#include "frontends/wx/wx_frame.hpp"
|
||||
|
||||
#include "frontends/wx/wx_frontend.hpp"
|
||||
|
||||
#include "core/app/export.hpp"
|
||||
#include "core/app/load.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/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 <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
enum {
|
||||
ID_LOAD = wxID_HIGHEST + 1,
|
||||
ID_RESTORE,
|
||||
ID_SAVE,
|
||||
ID_EXPORT,
|
||||
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()); }
|
||||
} // 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_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 *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(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);
|
||||
|
||||
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);
|
||||
|
||||
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::OnExport, this, ID_EXPORT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
|
||||
|
||||
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));
|
||||
std::vector<std::string> parts;
|
||||
for (auto &pkv : *m) parts.push_back(pkv.first);
|
||||
std::sort(parts.begin(), parts.end());
|
||||
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) + "]";
|
||||
tree_->AppendItem(mid, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
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::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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user