wx: drive the edit ops from the tree selection + a right-click menu

Each tree node now carries a NodeData (kind + Module/Part/Signal pointers), so
edits can act on what's selected instead of always re-asking:
  - Set connector type / Attach BSDL / Connect act on the selected Part (or the
    Pin's owning part); Connect uses it as the first endpoint, then prompts for
    the second. Set signal type acts on the selected Signal.
  - With nothing relevant selected, each falls back to its modal picker, so the
    menu-driven flow still works.
  - Right-click a Part or Signal → a context menu of the actions valid for it;
    the items reuse the menu IDs and select the clicked node first, so they run
    the same handlers.

wx-only; builds clean, window opens with no asserts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:38:23 +02:00
parent d4eac9557b
commit 184b0d306f
2 changed files with 107 additions and 20 deletions

View File

@@ -74,6 +74,39 @@ 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)
@@ -145,6 +178,8 @@ EssimFrame::EssimFrame(WxFrontend &fe)
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();
}
@@ -173,6 +208,11 @@ void EssimFrame::RebuildModelView() {
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;
@@ -185,6 +225,12 @@ void EssimFrame::RebuildModelView() {
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);
@@ -202,7 +248,11 @@ void EssimFrame::RebuildModelView() {
pl += ", dropped";
pl += ")";
}
tree_->AppendItem(pid, pl);
wxTreeItemId nid = tree_->AppendItem(pid, pl);
auto *d = new NodeData(NodeData::Kind::Pin);
d->module = m;
d->part = p;
tree_->SetItemData(nid, d);
}
}
@@ -215,8 +265,13 @@ void EssimFrame::RebuildModelView() {
std::sort(sigs.begin(), sigs.end(), natural_less);
for (const auto &sname : sigs) {
Signal *s = m->signals->get(sname);
tree_->AppendItem(sid, wx(sname) + type_suffix(s->type)
+ wxString::Format(" — %d pin(s)", (int)s->size()));
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);
}
}
@@ -368,7 +423,8 @@ Part *EssimFrame::PickPart(const wxString &caption) {
void EssimFrame::OnSetConnectorType(wxCommandEvent &) {
Part *p = PickPart();
Part *p = selected_part(tree_);
if (!p) p = PickPart();
if (!p) return;
wxTextEntryDialog dlg(this, "Connector type (empty = none):",
@@ -392,7 +448,8 @@ void EssimFrame::OnSetConnectorType(wxCommandEvent &) {
}
void EssimFrame::OnAttachBsdl(wxCommandEvent &) {
Part *p = PickPart();
Part *p = selected_part(tree_);
if (!p) p = PickPart();
if (!p) return;
wxFileDialog dlg(this, "Attach a BSDL model", "", "",
@@ -416,7 +473,8 @@ void EssimFrame::OnAttachBsdl(wxCommandEvent &) {
}
void EssimFrame::OnConnect(wxCommandEvent &) {
Part *p1 = PickPart("Connect — first part");
Part *p1 = selected_part(tree_);
if (!p1) p1 = PickPart("Connect — first part");
if (!p1) return;
Part *p2 = PickPart("Connect — second part");
if (!p2) return;
@@ -453,21 +511,25 @@ void EssimFrame::OnConnect(wxCommandEvent &) {
}
void EssimFrame::OnSetSignalType(wxCommandEvent &) {
Module *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;
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]);
}
std::vector<std::string> sigs;
for (auto &skv : *m->signals) sigs.push_back(skv.first);
std::sort(sigs.begin(), sigs.end());
wxArrayString schoices;
for (const auto &s : sigs) schoices.Add(wx(s));
int si = wxGetSingleChoiceIndex("Signal:", "Set signal type", schoices, this);
if (si < 0) return;
Signal *sig = m->signals->get(sigs[si]);
static const wxString types[] = {"power", "gnd", "other"};
int ti = wxGetSingleChoiceIndex("Type:", "Set signal type",
@@ -522,3 +584,22 @@ void EssimFrame::OnAbout(wxCommandEvent &) {
"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);
}

View File

@@ -7,6 +7,7 @@ class WxFrontend;
class wxTreeCtrl;
class wxTextCtrl;
class wxCommandEvent;
class wxTreeEvent;
// 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
@@ -30,6 +31,11 @@ private:
void OnQuit(wxCommandEvent &);
void OnAbout(wxCommandEvent &);
// Right-click on a tree item → context menu of the edit actions valid for
// that node (part / signal). The actions reuse the menu IDs, so they run
// the same handlers — which read the tree selection.
void OnTreeContextMenu(wxTreeEvent &);
// Modal pickers over the current System. `caption` titles the dialogs (e.g.
// to distinguish two picks). Each returns nullptr if there is nothing to
// pick or the user cancels.