diff --git a/.gitignore b/.gitignore index d163863..a5309e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -build/ \ No newline at end of file +build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 16caa8f..5636049 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,7 @@ target_link_libraries(essim_core # src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui. set(ESSIM_FRONTEND "tui" CACHE STRING "Frontend to build: a directory name under src/frontends/, or 'none'") -set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none) +set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui wx none) if(ESSIM_FRONTEND STREQUAL "none") message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)") diff --git a/src/frontends/wx/CMakeLists.txt b/src/frontends/wx/CMakeLists.txt new file mode 100644 index 0000000..6a0e24a --- /dev/null +++ b/src/frontends/wx/CMakeLists.txt @@ -0,0 +1,16 @@ +# wxWidgets GUI frontend. Builds the `essim` executable against essim_core. +# +# Self-contained like every frontend: it pulls its own GUI toolkit (here a +# system wxWidgets via find_package), then defers the target wiring to the +# shared essim_add_frontend() helper. Select it with -DESSIM_FRONTEND=wx. +# +# Needs the wxWidgets development package, e.g. on Debian/Ubuntu: +# sudo apt install libwxgtk3.2-dev + +find_package(wxWidgets REQUIRED COMPONENTS core base) + +# UsewxWidgets sets the include dirs and compile definitions for targets defined +# afterwards in this directory — so it must come before essim_add_frontend(). +include(${wxWidgets_USE_FILE}) + +essim_add_frontend(wx LIBS ${wxWidgets_LIBRARIES}) diff --git a/src/frontends/wx/main.cpp b/src/frontends/wx/main.cpp new file mode 100644 index 0000000..76f6ef9 --- /dev/null +++ b/src/frontends/wx/main.cpp @@ -0,0 +1,10 @@ +#include "frontends/frontend_main.hpp" +#include "frontends/wx/wx_frontend.hpp" + +// The wx frontend's entry point: construct the concrete Frontend (WxFrontend) +// and hand off to the shared, frontend-agnostic launcher. Identical in shape to +// the tui frontend's main — only the Frontend type differs. +int main(int argc, char **argv) { + WxFrontend fe; + return frontend_main(argc, argv, fe); +} diff --git a/src/frontends/wx/wx_frame.cpp b/src/frontends/wx/wx_frame.cpp new file mode 100644 index 0000000..f7b0776 --- /dev/null +++ b/src/frontends/wx/wx_frame.cpp @@ -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 +#include +#include +#include +#include +#include + +#include +#include +#include + +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 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 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 &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); +} diff --git a/src/frontends/wx/wx_frame.hpp b/src/frontends/wx/wx_frame.hpp new file mode 100644 index 0000000..31fcc37 --- /dev/null +++ b/src/frontends/wx/wx_frame.hpp @@ -0,0 +1,38 @@ +#ifndef _WX_FRAME_HPP_ +#define _WX_FRAME_HPP_ + +#include + +class WxFrontend; +class wxTreeCtrl; +class wxTextCtrl; +class wxCommandEvent; + +// The essim main window. Holds no domain state of its own: it reads and mutates +// the System owned by the WxFrontend, calling the core/app operations directly +// (load, verify, export, save, restore) and rendering their results into a +// model tree, an overview panel and a log. +class EssimFrame : public wxFrame { +public: + explicit EssimFrame(WxFrontend &fe); + +private: + // Menu handlers — each is a thin wrapper over a core/app operation. + void OnLoad(wxCommandEvent &); + void OnRestore(wxCommandEvent &); + void OnSave(wxCommandEvent &); + void OnExport(wxCommandEvent &); + void OnVerify(wxCommandEvent &); + void OnQuit(wxCommandEvent &); + void OnAbout(wxCommandEvent &); + + void RebuildModelView(); ///< refresh tree + overview from the System + void Log(const wxString &line); ///< append a line to the log pane + + WxFrontend &fe_; + wxTreeCtrl *tree_ = nullptr; + wxTextCtrl *overview_ = nullptr; + wxTextCtrl *log_ = nullptr; +}; + +#endif // _WX_FRAME_HPP_ diff --git a/src/frontends/wx/wx_frontend.cpp b/src/frontends/wx/wx_frontend.cpp new file mode 100644 index 0000000..9234469 --- /dev/null +++ b/src/frontends/wx/wx_frontend.cpp @@ -0,0 +1,105 @@ +#include "frontends/wx/wx_frontend.hpp" + +#include "frontends/wx/wx_frame.hpp" + +#include "core/domain/connect.hpp" +#include "core/domain/modules.hpp" +#include "core/domain/persist.hpp" +#include "core/domain/system.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace { + +// Minimal wxApp: on init it shows the main window bound to the frontend. +class EssimApp : public wxApp { +public: + explicit EssimApp(WxFrontend &fe) : fe_(fe) {} + bool OnInit() override { + // Decode the UTF-8 in our narrow string literals (em dash, ellipsis…) + // correctly: wxString converts const char* via the C locale, which is + // "C" (ASCII) at startup. Set only LC_CTYPE — leave LC_NUMERIC as "C" + // so number formatting stays dot-decimal. + std::setlocale(LC_CTYPE, ""); + (new EssimFrame(fe_))->Show(true); + return true; + } + +private: + WxFrontend &fe_; +}; + +} // namespace + +WxFrontend::WxFrontend() = default; +WxFrontend::~WxFrontend() = default; + +void WxFrontend::ensure_system() { + if (!sys_) sys_.reset(new System()); +} + +void WxFrontend::set_system(System *fresh) { + sys_.reset(fresh); +} + +void WxFrontend::BootDispatch(const std::string &raw) { + // The GUI has no command shell. Honour the boot commands that make sense + // headlessly: `restore ` seeds a snapshot; anything else is noted. + std::istringstream iss(raw); + std::string cmd; + iss >> cmd; + std::string arg; + std::getline(iss, arg); + if (std::size_t b = arg.find_first_not_of(" \t"); b != std::string::npos) + arg = arg.substr(b); + else + arg.clear(); + + if (cmd == "restore") { + std::string err; + System *fresh = restore_system(arg, err); + if (!fresh) { + output_ += "restore failed: " + err + "\n"; + return; + } + sys_.reset(fresh); + output_ += "restored from " + arg + " (" + + std::to_string(sys_->modules()->size()) + " module(s), " + + std::to_string(sys_->connections()->size()) + + " connection(s))\n"; + } else if (cmd == "source") { + output_ += "source: the wx frontend has no script interpreter " + "(use the tui frontend for scripts).\n"; + } else if (!cmd.empty()) { + output_ += "boot: ignored '" + raw + "'.\n"; + } +} + +void WxFrontend::DumpCommandsMd(std::ostream &out) const { + out << "# essim — wx frontend\n\n" + << "The wx frontend is menu-driven and exposes no textual command " + << "registry. Generate the command reference from the tui frontend " + << "(`-DESSIM_FRONTEND=tui`, then `essim --commands-md`).\n"; +} + +void WxFrontend::DumpOutput(std::ostream &out) const { + out << output_; +} + +void WxFrontend::Run() { + ensure_system(); + + wxApp::SetInstance(new EssimApp(*this)); + int argc = 0; + wxEntryStart(argc, static_cast(nullptr)); + if (wxTheApp->CallOnInit()) + wxTheApp->OnRun(); + wxTheApp->OnExit(); + wxEntryCleanup(); +} diff --git a/src/frontends/wx/wx_frontend.hpp b/src/frontends/wx/wx_frontend.hpp new file mode 100644 index 0000000..1545d22 --- /dev/null +++ b/src/frontends/wx/wx_frontend.hpp @@ -0,0 +1,37 @@ +#ifndef _WX_FRONTEND_HPP_ +#define _WX_FRONTEND_HPP_ + +#include "frontends/frontend.hpp" + +#include +#include + +class System; + +// wxWidgets GUI frontend. Implements the shared Frontend interface so the same +// launcher (frontend_main) drives it: it owns the System and a console buffer, +// handles boot commands headlessly (for --restore/--batch), and Run() opens the +// wxWidgets window. The window itself (EssimFrame) drives essim_core / app::* +// operations directly — no command shell, no TUI reuse. +class WxFrontend : public Frontend { +public: + WxFrontend(); + ~WxFrontend() override; + + // --- Frontend interface --- + void BootDispatch(const std::string &raw) override; + void DumpCommandsMd(std::ostream &out) const override; + void DumpOutput(std::ostream &out) const override; + void Run() override; + + // --- used by the window (EssimFrame) --- + System *system() const { return sys_.get(); } + void set_system(System *fresh); ///< take ownership (used by Restore) + void ensure_system(); ///< create an empty System if none yet + +private: + std::unique_ptr sys_; + std::string output_; ///< console buffer surfaced by DumpOutput (batch) +}; + +#endif // _WX_FRONTEND_HPP_