From 61a4016a11f6467caf04724f5f8828bc90ee0837 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 26 Aug 2026 20:58:46 +0100 Subject: [PATCH 1/2] Tidy and standardise dialog layout and styling. --- Makefile.am | 6 +- src/gui/dleditnode.cc | 41 ++-- src/gui/dlefglayout.cc | 86 +++++---- src/gui/dlefglegend.cc | 50 ++--- src/gui/dlefglogit.cc | 413 ---------------------------------------- src/gui/dlefglogit.h | 56 ------ src/gui/dlefgreveal.cc | 10 +- src/gui/dlgameprop.cc | 60 +++--- src/gui/dlnash.cc | 21 ++- src/gui/dlnashmon.cc | 23 ++- src/gui/dlnewtable.cc | 12 +- src/gui/dlnfglogit.cc | 420 ----------------------------------------- src/gui/dlnfglogit.h | 58 ------ src/gui/gameframe.cc | 4 +- 14 files changed, 162 insertions(+), 1098 deletions(-) delete mode 100644 src/gui/dlefglogit.cc delete mode 100644 src/gui/dlefglogit.h delete mode 100644 src/gui/dlnfglogit.cc delete mode 100644 src/gui/dlnfglogit.h diff --git a/Makefile.am b/Makefile.am index bff1ba7cc0..f19f2d6138 100644 --- a/Makefile.am +++ b/Makefile.am @@ -463,8 +463,6 @@ gambit_SOURCES = \ src/gui/dlefglayout.h \ src/gui/dlefglegend.cc \ src/gui/dlefglegend.h \ - src/gui/dlefglogit.cc \ - src/gui/dlefglogit.h \ src/gui/dlefgreveal.cc \ src/gui/dlexcept.h \ src/gui/dlgameprop.cc \ @@ -476,8 +474,8 @@ gambit_SOURCES = \ src/gui/nashspec.h \ src/gui/dlnewtable.cc \ src/gui/dlnewtable.h \ - src/gui/dlnfglogit.cc \ - src/gui/dlnfglogit.h \ + src/gui/dllogit.cc \ + src/gui/dllogit.h \ src/gui/efgdisplay.cc \ src/gui/efgdisplay.h \ src/gui/efgdragdrop.cc \ diff --git a/src/gui/dleditnode.cc b/src/gui/dleditnode.cc index 9165c38232..f69378cf46 100644 --- a/src/gui/dleditnode.cc +++ b/src/gui/dleditnode.cc @@ -34,19 +34,23 @@ namespace Gambit::GUI { //====================================================================== EditNodeDialog::EditNodeDialog(wxWindow *p_parent, const GameNode &p_node) - : wxDialog(p_parent, wxID_ANY, _("Node properties"), wxDefaultPosition), m_node(p_node) + : wxDialog(p_parent, wxID_ANY, _("Node properties"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_node(p_node) { + const int S = FromDIP(5); + auto *topSizer = new wxBoxSizer(wxVERTICAL); auto *labelSizer = new wxBoxSizer(wxHORIZONTAL); - labelSizer->Add(new wxStaticText(this, wxID_STATIC, _("Node label")), 0, wxALL | wxCENTER, 5); + labelSizer->Add(new wxStaticText(this, wxID_STATIC, _("Node label")), 0, wxALL | wxCENTER, S); m_nodeLabel = new LabelTextCtrl(this, wxID_ANY, wxString::FromUTF8(m_node->GetLabel())); - labelSizer->Add(m_nodeLabel, 1, wxALL | wxCENTER | wxEXPAND, 5); - topSizer->Add(labelSizer, 0, wxALL | wxEXPAND, 5); + labelSizer->Add(m_nodeLabel, 1, wxALL | wxCENTER | wxEXPAND, S); + topSizer->Add(labelSizer, 0, wxALL | wxEXPAND, S); auto *infosetSizer = new wxBoxSizer(wxHORIZONTAL); infosetSizer->Add(new wxStaticText(this, wxID_STATIC, _("Information set")), 0, wxALL | wxCENTER, - 5); + S); m_infoset = new wxChoice(this, wxID_ANY); if (!p_node->IsTerminal()) { m_infoset->Append(_("New information set")); @@ -89,23 +93,23 @@ EditNodeDialog::EditNodeDialog(wxWindow *p_parent, const GameNode &p_node) m_infoset->SetSelection(0); m_infoset->Enable(false); } - infosetSizer->Add(m_infoset, 1, wxALL | wxEXPAND, 5); - topSizer->Add(infosetSizer, 0, wxALL | wxEXPAND, 5); + infosetSizer->Add(m_infoset, 1, wxALL | wxEXPAND, S); + topSizer->Add(infosetSizer, 0, wxALL | wxEXPAND, S); auto *subgameSizer = new wxBoxSizer(wxVERTICAL); if (!p_node->GetParent()) { subgameSizer->Add(new wxStaticText(this, wxID_STATIC, _("This is the root node of the tree")), - 0, wxALL | wxCENTER, 5); + 0, wxALL | wxCENTER, S); } else if (p_node->IsSubgameRoot()) { subgameSizer->Add( new wxStaticText(this, wxID_STATIC, _("This is the root of a proper subgame")), 0, - wxALL | wxCENTER, 5); + wxALL | wxCENTER, S); } - topSizer->Add(subgameSizer, 0, wxALL | wxCENTER, 5); + topSizer->Add(subgameSizer, 0, wxALL | wxCENTER, S); auto *outcomeSizer = new wxBoxSizer(wxHORIZONTAL); - outcomeSizer->Add(new wxStaticText(this, wxID_STATIC, _("Outcome")), 0, wxALL | wxCENTER, 5); + outcomeSizer->Add(new wxStaticText(this, wxID_STATIC, _("Outcome")), 0, wxALL | wxCENTER, S); m_outcome = new wxChoice(this, wxID_ANY); m_outcome->Append(_("(null)")); m_outcome->SetSelection(0); @@ -137,15 +141,12 @@ EditNodeDialog::EditNodeDialog(wxWindow *p_parent, const GameNode &p_node) m_outcome->SetSelection(outcome->GetNumber()); } } - outcomeSizer->Add(m_outcome, 1, wxALL | wxEXPAND, 5); - topSizer->Add(outcomeSizer, 0, wxALL | wxEXPAND, 5); - - auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); - buttonSizer->Add(new wxButton(this, wxID_CANCEL, _("Cancel")), 0, wxALL, 5); - auto *okButton = new wxButton(this, wxID_OK, _("OK")); - okButton->SetDefault(); - buttonSizer->Add(okButton, 0, wxALL, 5); - topSizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); + outcomeSizer->Add(m_outcome, 1, wxALL | wxEXPAND, S); + topSizer->Add(outcomeSizer, 0, wxALL | wxEXPAND, S); + + if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { + topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); + } SetSizer(topSizer); topSizer->Fit(this); diff --git a/src/gui/dlefglayout.cc b/src/gui/dlefglayout.cc index 429a38d1b2..1427aa9bdf 100644 --- a/src/gui/dlefglayout.cc +++ b/src/gui/dlefglayout.cc @@ -61,6 +61,8 @@ class LayoutNodesPanel final : public wxPanel { LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p_settings) : wxPanel(p_parent, wxID_ANY) { + const int S = FromDIP(5); + auto *topSizer = new wxBoxSizer(wxVERTICAL); auto *nodeSizer = new wxStaticBoxSizer(wxVERTICAL, this, _T("Drawing nodes")); @@ -71,26 +73,26 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p _("a diamond"), _("a filled circle")}; tokenSizer->Add(new wxStaticText(this, wxID_STATIC, _("Indicate chance nodes with")), 0, - wxALL | wxALIGN_CENTER_VERTICAL, 5); + wxALL | wxALIGN_CENTER_VERTICAL, S); m_chanceToken = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, tokenChoices); m_chanceToken->SetSelection(p_settings.GetChanceToken()); - tokenSizer->Add(m_chanceToken, 1, wxALL | wxEXPAND, 5); + tokenSizer->Add(m_chanceToken, 1, wxALL | wxEXPAND, S); tokenSizer->Add(new wxStaticText(this, wxID_STATIC, _("Indicate player nodes with")), 0, - wxALL | wxALIGN_CENTER_VERTICAL, 5); + wxALL | wxALIGN_CENTER_VERTICAL, S); m_playerToken = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, tokenChoices); m_playerToken->SetSelection(p_settings.GetPlayerToken()); - tokenSizer->Add(m_playerToken, 1, wxALL | wxEXPAND, 5); + tokenSizer->Add(m_playerToken, 1, wxALL | wxEXPAND, S); tokenSizer->Add(new wxStaticText(this, wxID_STATIC, _("Indicate terminal nodes with")), 0, - wxALL | wxALIGN_CENTER_VERTICAL, 5); + wxALL | wxALIGN_CENTER_VERTICAL, S); m_terminalToken = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, tokenChoices); m_terminalToken->SetSelection(p_settings.GetTerminalToken()); - tokenSizer->Add(m_terminalToken, 1, wxALL | wxEXPAND, 5); + tokenSizer->Add(m_terminalToken, 1, wxALL | wxEXPAND, S); - nodeSizer->Add(tokenSizer, 1, wxALL | wxEXPAND, 5); - topSizer->Add(nodeSizer, 0, wxALL | wxALIGN_CENTER, 5); + nodeSizer->Add(tokenSizer, 1, wxALL | wxEXPAND, S); + topSizer->Add(nodeSizer, 0, wxALL | wxALIGN_CENTER, S); auto *sizeSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Layout sizing")); @@ -98,7 +100,7 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p gridSizer->AddGrowableCol(1); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Horizontal size of nodes")), 0, - wxALIGN_CENTER_VERTICAL | wxALL, 5); + wxALIGN_CENTER_VERTICAL | wxALL, S); { constexpr int NODE_LENGTH_MIN = 5; @@ -107,11 +109,11 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p label << p_settings.GetNodeSize(); m_nodeSize = new wxSpinCtrl(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, NODE_LENGTH_MIN, NODE_LENGTH_MAX); - gridSizer->Add(m_nodeSize, 1, wxEXPAND | wxALL, 5); + gridSizer->Add(m_nodeSize, 1, wxEXPAND | wxALL, S); } gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Vertical spacing between terminal nodes")), 0, - wxALIGN_CENTER_VERTICAL | wxALL, 5); + wxALIGN_CENTER_VERTICAL | wxALL, S); { constexpr int Y_SPACING_MIN = 15; @@ -120,11 +122,11 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p label << p_settings.GetTerminalSpacing(); m_terminalSpacing = new wxSpinCtrl(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, Y_SPACING_MIN, Y_SPACING_MAX); - gridSizer->Add(m_terminalSpacing, 1, wxEXPAND | wxALL, 5); + gridSizer->Add(m_terminalSpacing, 1, wxEXPAND | wxALL, S); } - sizeSizer->Add(gridSizer, 1, wxALL | wxEXPAND, 5); - topSizer->Add(sizeSizer, 0, wxALL | wxALIGN_CENTER, 5); + sizeSizer->Add(gridSizer, 1, wxALL | wxEXPAND, S); + topSizer->Add(sizeSizer, 0, wxALL | wxALIGN_CENTER, S); SetSizer(topSizer); topSizer->Fit(this); @@ -155,6 +157,8 @@ class LayoutBranchesPanel : public wxPanel { LayoutBranchesPanel::LayoutBranchesPanel(wxWindow *p_parent, const TreeRenderConfig &p_settings) : wxPanel(p_parent, wxID_ANY) { + const int S = FromDIP(5); + auto *topSizer = new wxBoxSizer(wxVERTICAL); auto *styleBoxSizer = new wxStaticBoxSizer(wxHORIZONTAL, this, _("Drawing branches")); @@ -162,22 +166,22 @@ LayoutBranchesPanel::LayoutBranchesPanel(wxWindow *p_parent, const TreeRenderCon auto *styleSizer = new wxFlexGridSizer(2); styleSizer->Add(new wxStaticText(this, wxID_STATIC, _("Draw branches")), 0, - wxALL | wxALIGN_CENTER_VERTICAL, 5); + wxALL | wxALIGN_CENTER_VERTICAL, S); wxString styleChoices[] = {_("using straight lines between nodes"), _("with a tine for branch labels")}; m_branchStyle = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, styleChoices); m_branchStyle->SetSelection(p_settings.GetBranchStyle()); - styleSizer->Add(m_branchStyle, 1, wxALL | wxEXPAND, 5); + styleSizer->Add(m_branchStyle, 1, wxALL | wxEXPAND, S); styleSizer->Add(new wxStaticText(this, wxID_STATIC, _("Draw labels")), 1, - wxALL | wxALIGN_CENTER_VERTICAL, 5); + wxALL | wxALIGN_CENTER_VERTICAL, S); wxString labelChoices[] = {_("horizontally"), _("rotated parallel to the branch")}; m_branchLabels = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, labelChoices); m_branchLabels->SetSelection(p_settings.GetBranchLabels()); - styleSizer->Add(m_branchLabels, 1, wxALL | wxEXPAND, 5); + styleSizer->Add(m_branchLabels, 1, wxALL | wxEXPAND, S); - styleBoxSizer->Add(styleSizer, 1, wxALL | wxEXPAND, 5); - topSizer->Add(styleBoxSizer, 0, wxALL | wxALIGN_CENTER, 5); + styleBoxSizer->Add(styleSizer, 1, wxALL | wxEXPAND, S); + topSizer->Add(styleBoxSizer, 0, wxALL | wxALIGN_CENTER, S); auto *lengthSizer = new wxStaticBoxSizer(wxHORIZONTAL, this, _T("size of branches")); @@ -188,28 +192,28 @@ LayoutBranchesPanel::LayoutBranchesPanel(wxWindow *p_parent, const TreeRenderCon constexpr int BRANCH_LENGTH_MIN = 0; constexpr int BRANCH_LENGTH_MAX = 100; gridSizer->Add(new wxStaticText(this, wxID_ANY, _("size of branch fork")), 0, - wxALIGN_CENTER_VERTICAL | wxALL, 5); + wxALIGN_CENTER_VERTICAL | wxALL, S); wxString label; label << p_settings.GetBranchLength(); m_branchLength = new wxSpinCtrl(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, BRANCH_LENGTH_MIN, BRANCH_LENGTH_MAX); - gridSizer->Add(m_branchLength, 1, wxALL | wxEXPAND, 5); + gridSizer->Add(m_branchLength, 1, wxALL | wxEXPAND, S); } { constexpr int TINE_LENGTH_MIN = 20; constexpr int TINE_LENGTH_MAX = 100; gridSizer->Add(new wxStaticText(this, wxID_ANY, _("size of branch tine")), 1, - wxALIGN_CENTER_VERTICAL | wxALL, 5); + wxALIGN_CENTER_VERTICAL | wxALL, S); wxString label; label << p_settings.GetTineLength(); m_tineLength = new wxSpinCtrl(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, TINE_LENGTH_MIN, TINE_LENGTH_MAX); - gridSizer->Add(m_tineLength, 1, wxALL | wxEXPAND, 5); + gridSizer->Add(m_tineLength, 1, wxALL | wxEXPAND, S); } - lengthSizer->Add(gridSizer, 1, wxALL | wxEXPAND, 5); - topSizer->Add(lengthSizer, 0, wxALL | wxALIGN_CENTER, 5); + lengthSizer->Add(gridSizer, 1, wxALL | wxEXPAND, S); + topSizer->Add(lengthSizer, 0, wxALL | wxALIGN_CENTER, S); SetSizer(topSizer); topSizer->Fit(this); @@ -232,6 +236,8 @@ class LayoutInfosetsPanel final : public wxPanel { LayoutInfosetsPanel::LayoutInfosetsPanel(wxWindow *p_parent, const TreeRenderConfig &p_settings) : wxPanel(p_parent, wxID_ANY) { + const int S = FromDIP(5); + auto *topSizer = new wxBoxSizer(wxVERTICAL); auto *infosetSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Drawing information sets")); @@ -239,13 +245,13 @@ LayoutInfosetsPanel::LayoutInfosetsPanel(wxWindow *p_parent, const TreeRenderCon auto *styleSizer = new wxFlexGridSizer(2); styleSizer->Add(new wxStaticText(this, wxID_STATIC, _("Draw information set connections")), 0, - wxALL | wxALIGN_CENTER_VERTICAL, 5); + wxALL | wxALIGN_CENTER_VERTICAL, S); wxString joinChoices[] = {_("using lines"), _("using bubbles")}; m_infosetJoin = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, joinChoices); m_infosetJoin->SetSelection(p_settings.GetInfosetJoin()); - styleSizer->Add(m_infosetJoin, 0, wxALL | wxEXPAND, 5); - infosetSizer->Add(styleSizer, 0, wxALL | wxALIGN_CENTER, 5); - topSizer->Add(infosetSizer, 0, wxALL, 5); + styleSizer->Add(m_infosetJoin, 0, wxALL | wxEXPAND, S); + infosetSizer->Add(styleSizer, 0, wxALL | wxALIGN_CENTER, S); + topSizer->Add(infosetSizer, 0, wxALL, S); SetSizer(topSizer); topSizer->Fit(this); @@ -256,8 +262,12 @@ LayoutInfosetsPanel::LayoutInfosetsPanel(wxWindow *p_parent, const TreeRenderCon } // anonymous namespace LayoutDialog::LayoutDialog(wxWindow *p_parent, const TreeRenderConfig &p_settings) - : wxDialog(p_parent, wxID_ANY, _("Layout options"), wxDefaultPosition), m_toDefaults(false) + : wxDialog(p_parent, wxID_ANY, _("Layout options"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_toDefaults(false) { + const int S = FromDIP(5); + m_notebook = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_notebook->AddPage(new LayoutNodesPanel(m_notebook, p_settings), _("Nodes")); m_notebook->AddPage(new LayoutBranchesPanel(m_notebook, p_settings), _("Branches")); @@ -267,15 +277,15 @@ LayoutDialog::LayoutDialog(wxWindow *p_parent, const TreeRenderConfig &p_setting auto *defaultsButton = new wxButton(this, wxID_ANY, _("Set to defaults")); Connect(defaultsButton->GetId(), wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(LayoutDialog::OnSetDefaults)); - buttonSizer->Add(defaultsButton, 0, wxALL, 5); - buttonSizer->Add(new wxButton(this, wxID_CANCEL, _("Cancel")), 0, wxALL, 5); - auto *okButton = new wxButton(this, wxID_OK, _("OK")); - okButton->SetDefault(); - buttonSizer->Add(okButton, 0, wxALL, 5); + buttonSizer->Add(defaultsButton, 0, wxALL, S); + buttonSizer->AddStretchSpacer(); + if (auto *stdButtons = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { + buttonSizer->Add(stdButtons, 0, wxALL, S); + } auto *topSizer = new wxBoxSizer(wxVERTICAL); - topSizer->Add(m_notebook, 0, wxEXPAND | wxALL, 5); - topSizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); + topSizer->Add(m_notebook, 0, wxEXPAND | wxALL, S); + topSizer->Add(buttonSizer, 0, wxEXPAND | wxALL, S); SetSizer(topSizer); topSizer->Fit(this); diff --git a/src/gui/dlefglegend.cc b/src/gui/dlefglegend.cc index 01263c2614..7621da029e 100644 --- a/src/gui/dlefglegend.cc +++ b/src/gui/dlefglegend.cc @@ -33,8 +33,11 @@ namespace Gambit::GUI { //========================================================================== LegendDialog::LegendDialog(wxWindow *p_parent, const TreeRenderConfig &p_options) - : wxDialog(p_parent, wxID_ANY, _("Labels"), wxDefaultPosition) + : wxDialog(p_parent, wxID_ANY, _("Labels"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { + const int S = FromDIP(5); + auto *nodeGroup = new wxStaticBoxSizer(new wxStaticBox(this, wxID_ANY, _("Node labeling")), wxVERTICAL); @@ -49,23 +52,23 @@ LegendDialog::LegendDialog(wxWindow *p_parent, const TreeRenderConfig &p_options auto *nodeAboveSizer = new wxBoxSizer(wxHORIZONTAL); nodeAboveSizer->Add(new wxStaticText(this, wxID_STATIC, _("Display")), 0, wxALL | wxALIGN_CENTER, - 5); + S); m_nodeAbove = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 8, nodeLabelList); m_nodeAbove->SetSelection(p_options.GetNodeAboveLabel()); - nodeAboveSizer->Add(m_nodeAbove, 1, wxALL | wxALIGN_CENTER, 5); + nodeAboveSizer->Add(m_nodeAbove, 1, wxALL | wxALIGN_CENTER, S); nodeAboveSizer->Add(new wxStaticText(this, wxID_STATIC, _("above each node")), 0, - wxALL | wxALIGN_CENTER, 5); - nodeGroup->Add(nodeAboveSizer, 0, wxALL | wxEXPAND, 5); + wxALL | wxALIGN_CENTER, S); + nodeGroup->Add(nodeAboveSizer, 0, wxALL | wxEXPAND, S); auto *nodeBelowSizer = new wxBoxSizer(wxHORIZONTAL); nodeBelowSizer->Add(new wxStaticText(this, wxID_STATIC, _("Display")), 0, wxALL | wxALIGN_CENTER, - 5); + S); m_nodeBelow = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 8, nodeLabelList); m_nodeBelow->SetSelection(p_options.GetNodeBelowLabel()); - nodeBelowSizer->Add(m_nodeBelow, 1, wxALL | wxALIGN_CENTER, 5); + nodeBelowSizer->Add(m_nodeBelow, 1, wxALL | wxALIGN_CENTER, S); nodeBelowSizer->Add(new wxStaticText(this, wxID_STATIC, _("below each node")), 0, - wxALL | wxALIGN_CENTER, 5); - nodeGroup->Add(nodeBelowSizer, 0, wxALL | wxEXPAND, 5); + wxALL | wxALIGN_CENTER, S); + nodeGroup->Add(nodeBelowSizer, 0, wxALL | wxEXPAND, S); auto *actionGroup = new wxStaticBoxSizer(new wxStaticBox(this, wxID_ANY, _("Action labeling")), wxVERTICAL); @@ -76,36 +79,33 @@ LegendDialog::LegendDialog(wxWindow *p_parent, const TreeRenderConfig &p_options auto *actionAboveSizer = new wxBoxSizer(wxHORIZONTAL); actionAboveSizer->Add(new wxStaticText(this, wxID_STATIC, _("Display")), 0, - wxALL | wxALIGN_CENTER, 5); + wxALL | wxALIGN_CENTER, S); m_actionAbove = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 4, actionLabelList); m_actionAbove->SetSelection(p_options.GetBranchAboveLabel()); - actionAboveSizer->Add(m_actionAbove, 1, wxALL | wxALIGN_CENTER, 5); + actionAboveSizer->Add(m_actionAbove, 1, wxALL | wxALIGN_CENTER, S); actionAboveSizer->Add(new wxStaticText(this, wxID_STATIC, _("above each action")), 0, - wxALL | wxALIGN_CENTER, 5); - actionGroup->Add(actionAboveSizer, 0, wxALL | wxEXPAND, 5); + wxALL | wxALIGN_CENTER, S); + actionGroup->Add(actionAboveSizer, 0, wxALL | wxEXPAND, S); auto *actionBelowSizer = new wxBoxSizer(wxHORIZONTAL); actionBelowSizer->Add(new wxStaticText(this, wxID_STATIC, _("Display")), 0, - wxALL | wxALIGN_CENTER, 5); + wxALL | wxALIGN_CENTER, S); m_actionBelow = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 4, actionLabelList); m_actionBelow->SetSelection(p_options.GetBranchBelowLabel()); - actionBelowSizer->Add(m_actionBelow, 1, wxALL | wxALIGN_CENTER, 5); + actionBelowSizer->Add(m_actionBelow, 1, wxALL | wxALIGN_CENTER, S); actionBelowSizer->Add(new wxStaticText(this, wxID_STATIC, _("below each action")), 0, - wxALL | wxALIGN_CENTER, 5); - actionGroup->Add(actionBelowSizer, 0, wxALL | wxEXPAND, 5); + wxALL | wxALIGN_CENTER, S); + actionGroup->Add(actionBelowSizer, 0, wxALL | wxEXPAND, S); auto *topSizer = new wxBoxSizer(wxVERTICAL); - topSizer->Add(nodeGroup, 0, wxEXPAND | wxALL, 5); - topSizer->Add(actionGroup, 0, wxEXPAND | wxALL, 5); + topSizer->Add(nodeGroup, 0, wxEXPAND | wxALL, S); + topSizer->Add(actionGroup, 0, wxEXPAND | wxALL, S); - auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); - buttonSizer->Add(new wxButton(this, wxID_CANCEL, _("Cancel")), 0, wxALL, 5); - auto *okButton = new wxButton(this, wxID_OK, _("OK")); - okButton->SetDefault(); - buttonSizer->Add(okButton, 0, wxALL, 5); - topSizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); + if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { + topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); + } SetSizer(topSizer); topSizer->Fit(this); diff --git a/src/gui/dlefglogit.cc b/src/gui/dlefglogit.cc deleted file mode 100644 index 0bb3484dff..0000000000 --- a/src/gui/dlefglogit.cc +++ /dev/null @@ -1,413 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/gui/dlefglogit.cc -// Dialog for monitoring progress of logit equilibrium computation -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#include -#include - -#include -#ifndef WX_PRECOMP -#include -#endif // WX_PRECOMP -#include - -#include -#include "dlefglogit.h" -#include "solvers/logit/logit.h" - -namespace Gambit::GUI { - -//========================================================================= -// class LogitBehavTable -//========================================================================= - -//! -//! Data model for the logit correspondence display: one row per -//! computed profile, received incrementally from the solver thread. -//! Row growth is deliberately batched (see AddProfile) so the grid -//! isn't resized/repainted on every single profile received. -//! -class LogitBehavTable final : public wxGridTableBase { - std::shared_ptr m_doc; - Array m_lambdas; - Array>> m_profiles; - int m_numCols{0}; - int m_numRows{0}; - -public: - explicit LogitBehavTable(const std::shared_ptr &p_doc) : m_doc(p_doc) {} - - int GetNumberRows() override { return m_numRows; } - int GetNumberCols() override { return m_numCols; } - bool IsEmptyCell(int, int) override { return false; } - wxString GetValue(int row, int col) override; - void SetValue(int, int, const wxString &) override {} - wxGridCellAttr *GetAttr(int row, int col, wxGridCellAttr::wxAttrKind kind) override; - - //! - //! wxGrid::SetColLabelValue/SetCornerLabelValue on the grid just forward - //! to the table's own (default no-op) methods of the same name, so - //! headers must be supplied by overriding the getters here instead. - //! - wxString GetColLabelValue(int col) override; - wxString GetCornerLabelValue() const override { return wxT("#"); } - - void AddProfile(const LogitQREMixedBehaviorProfile &p_qre, bool p_forceShow); -}; - -wxString LogitBehavTable::GetColLabelValue(int col) -{ - if (col == 0) { - return wxT("Lambda"); - } - - const GameAction action = m_doc->GetAction(col); - wxString label; - label << action->GetInfoset()->GetNumber() << ": " << action->GetLabel(); - return label; -} - -wxString LogitBehavTable::GetValue(int row, int col) -{ - if (col == 0) { - return {lexical_cast(m_lambdas[row + 1], m_doc->GetStyle().NumDecimals()).c_str(), - *wxConvCurrent}; - } - - const auto profile = m_profiles[row + 1]; - return {lexical_cast((*profile)[col], m_doc->GetStyle().NumDecimals()).c_str(), - *wxConvCurrent}; -} - -wxGridCellAttr *LogitBehavTable::GetAttr(int row, int col, wxGridCellAttr::wxAttrKind) -{ - auto *attr = new wxGridCellAttr(); - attr->SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL)); - attr->SetAlignment(wxALIGN_RIGHT, wxALIGN_CENTRE); - attr->SetReadOnly(true); - - if (col > 0) { - const GameAction action = m_doc->GetAction(col); - attr->SetTextColour(m_doc->GetStyle().GetPlayerColor(action->GetInfoset()->GetPlayer())); - attr->SetBackgroundColour(action->GetInfoset()->GetNumber() % 2 == 0 - ? wxColour(250, 250, 250) - : wxColour(225, 225, 225)); - } - else { - attr->SetTextColour(*wxBLACK); - attr->SetBackgroundColour(wxColour(250, 250, 250)); - } - - return attr; -} - -void LogitBehavTable::AddProfile(const LogitQREMixedBehaviorProfile &p_qre, bool p_forceShow) -{ - wxGrid *view = GetView(); - - if (m_numCols == 0) { - m_numCols = static_cast(m_doc->GetGame()->BehavProfileLength()) + 1; - if (view) { - wxGridTableMessage msg(this, wxGRIDTABLE_NOTIFY_COLS_APPENDED, m_numCols); - view->ProcessTableMessage(msg); - } - } - - const auto profile = std::make_shared>(m_doc->GetGame()); - m_lambdas.push_back(p_qre.GetLambda()); - for (size_t i = 1; i <= profile->BehaviorProfileLength(); i++) { - (*profile)[i] = p_qre[i]; - } - m_profiles.push_back(profile); - - if (p_forceShow || static_cast(m_profiles.size()) - m_numRows > 20) { - const int newRows = static_cast(m_profiles.size()) - m_numRows; - m_numRows = static_cast(m_profiles.size()); - if (view) { - wxGridTableMessage msg(this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, newRows); - view->ProcessTableMessage(msg); - view->MakeCellVisible(m_numRows - 1, 0); - } - } - - // Lambda tends to get large, so this column usually needs resized - if (view) { - view->AutoSizeColumn(0); - } -} - -//========================================================================= -// class LogitBehavGrid -//========================================================================= - -class LogitBehavGrid final : public wxGrid { - LogitBehavTable *m_gridTable; - - //! - //! @name Overriding wxGrid behavior to disable selection & the cursor - //! highlight rectangle, matching the read-only, non-interactive table - //! this replaces. - //! - //@{ - void OnRangeSelecting(wxGridRangeSelectEvent &p_event) { p_event.Veto(); } - void OnSelectCell(wxGridEvent &p_event) { p_event.Skip(); } - //@} - -public: - LogitBehavGrid(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxGrid(p_parent, wxID_ANY), m_gridTable(new LogitBehavTable(p_doc)) - { - SetTable(m_gridTable, true); - EnableEditing(false); - SetRowLabelSize(40); - SetColLabelSize(25); - - SetCellHighlightPenWidth(0); - SetCellHighlightROPenWidth(0); - - Bind(wxEVT_GRID_RANGE_SELECTING, &LogitBehavGrid::OnRangeSelecting, this); - Bind(wxEVT_GRID_SELECT_CELL, &LogitBehavGrid::OnSelectCell, this); - } - - void AddProfile(const LogitQREMixedBehaviorProfile &p_qre, bool p_forceShow) - { - m_gridTable->AddProfile(p_qre, p_forceShow); - } -}; - -namespace { - -wxDECLARE_EVENT(wxEVT_LOGIT_POINT, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_LOGIT_POINT, wxThreadEvent); - -wxDECLARE_EVENT(wxEVT_LOGIT_FINISHED, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_LOGIT_FINISHED, wxThreadEvent); - -#include "bitmaps/stop.xpm" - -} // namespace - -// -// Traces the logit equilibrium correspondence directly, in-process, on a -// worker thread, rather than shelling out to gambit-logit and parsing its -// output. Posts each point on the branch back to the dialog as it is found. -// -class LogitBehavThreadRunner final : public wxThread { - wxEvtHandler *m_parent; - Game m_game; - CancelToken m_cancel; - - void PostPoint(const LogitQREMixedBehaviorProfile &p_qre) const - { - auto *event = new wxThreadEvent(wxEVT_LOGIT_POINT); - event->SetPayload(p_qre); - wxQueueEvent(m_parent, event); - } - - ExitCode Entry() override - { - int exitCode = 0; - try { - const LogitQREMixedBehaviorProfile start(m_game); - LogitBehaviorSolve( - start, 1.0e-8, 1.0, 0.03, 1.1, Nash::NullBehaviorCallback, - [this](const LogitEvent &p_event) { - PostPoint(std::get>(p_event).qre); - }, - m_cancel); - } - catch (const ComputationCanceledException &) { - // Not an error -- LogitBehavDialog's m_stopRequested flag (already set - // before RequestCancel() was called) is what determines that the - // "finished" event below is reported as a stop rather than a failure. - } - catch (const std::exception &) { - exitCode = 1; - } - - auto *evt = new wxThreadEvent(wxEVT_LOGIT_FINISHED); - evt->SetInt(exitCode); - wxQueueEvent(m_parent, evt); - return static_cast(0); - } - -public: - LogitBehavThreadRunner(wxEvtHandler *p_parent, Game p_game) - : wxThread(wxTHREAD_JOINABLE), m_parent(p_parent), m_game(std::move(p_game)) - { - } - - void RequestCancel() { m_cancel.RequestCancel(); } -}; - -LogitBehavDialog::LogitBehavDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxDialog(p_parent, wxID_ANY, wxT("Compute quantal response equilibria"), wxDefaultPosition), - m_doc(p_doc), m_behavList(new LogitBehavGrid(this, m_doc)) -{ - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *startSizer = new wxBoxSizer(wxHORIZONTAL); - - m_statusText = - new wxStaticText(this, wxID_STATIC, wxT("The computation is currently in progress.")); - m_statusText->SetForegroundColour(*wxBLUE); - startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, 5); - - m_stopButton = new wxBitmapButton(this, wxID_CANCEL, wxBitmap(stop_xpm)); - m_stopButton->SetToolTip(_("Stop the computation")); - startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, 5); - m_stopButton->Bind(wxEVT_BUTTON, &LogitBehavDialog::OnStop, this); - - sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, 5); - - m_behavList->SetSizeHints(wxSize(600, 400)); - sizer->Add(m_behavList, 0, wxALL | wxALIGN_CENTER, 5); - - auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); - m_saveButton = new wxButton(this, wxID_SAVE, wxT("Save correspondence to .csv file")); - m_saveButton->Enable(false); - buttonSizer->Add(m_saveButton, 0, wxALL | wxALIGN_CENTER, 5); - m_saveButton->Bind(wxEVT_BUTTON, &LogitBehavDialog::OnSave, this); - - m_okButton = new wxButton(this, wxID_OK, wxT("OK")); - buttonSizer->Add(m_okButton, 0, wxALL | wxALIGN_CENTER, 5); - m_okButton->Enable(false); - - sizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); - - SetSizer(sizer); - sizer->Fit(this); - sizer->SetSizeHints(this); - wxTopLevelWindowBase::Layout(); - CenterOnParent(); - - Bind(wxEVT_LOGIT_POINT, &LogitBehavDialog::OnRunnerPoint, this); - Bind(wxEVT_LOGIT_FINISHED, &LogitBehavDialog::OnRunnerFinished, this); - Bind(wxEVT_CLOSE_WINDOW, &LogitBehavDialog::OnClose, this); - - Start(); -} - -LogitBehavDialog::~LogitBehavDialog() = default; - -void LogitBehavDialog::Start() -{ - // GameRep's lazy caches are not synchronized against concurrent first - // touch from two threads, so force the sequence-form derivation the - // worker will need on this (the main) thread before handing the game - // over. Constructing a profile forces EnsureInfosetOrdering() via the - // same public code path the worker's own starting profile will use. - const MixedBehaviorProfile warmup(m_doc->GetGame()); - m_doc->GetGame()->EnsureSequences(); - - m_runner = std::make_unique(this, m_doc->GetGame()); - if (m_runner->Run() != wxTHREAD_NO_ERROR) { - m_runner.reset(); - m_statusText->SetLabel(wxT("Failed to launch computation.")); - m_statusText->SetForegroundColour(*wxRED); - m_okButton->Enable(true); - } -} - -void LogitBehavDialog::OnRunnerPoint(wxThreadEvent &p_event) -{ - const auto &qre = p_event.GetPayload(); - m_behavList->AddProfile(qre, false); - - const int decimals = m_doc->GetStyle().NumDecimals(); - wxString line(lexical_cast(qre.GetLambda(), decimals).c_str(), *wxConvCurrent); - for (size_t i = 1; i <= qre.size(); i++) { - line += - wxT(",") + wxString(lexical_cast(qre[i], decimals).c_str(), *wxConvCurrent); - } - m_output += line + wxT("\n"); -} - -void LogitBehavDialog::OnRunnerFinished(wxThreadEvent &p_event) -{ - m_stopButton->Enable(false); - - if (m_runner) { - // Joinable thread: must be waited on before it can be safely destroyed. - // Entry() has already posted this event and is returning, so this - // should not block for any appreciable time. - m_runner->Wait(); - m_runner.reset(); - } - - if (m_stopRequested) { - m_statusText->SetLabel(wxT("The computation has been stopped.")); - m_statusText->SetForegroundColour(wxColour(196, 128, 0)); - } - else if (p_event.GetInt() == 0) { - m_statusText->SetLabel(wxT("The computation has completed.")); - m_statusText->SetForegroundColour(wxColour(0, 192, 0)); - } - else { - m_statusText->SetLabel(wxT("The computation ended abnormally.")); - m_statusText->SetForegroundColour(*wxRED); - } - - m_okButton->Enable(true); - m_saveButton->Enable(true); -} - -void LogitBehavDialog::OnStop(wxCommandEvent &) -{ - m_stopRequested = true; - m_stopButton->Enable(false); - if (m_runner) { - m_runner->RequestCancel(); - } -} - -void LogitBehavDialog::OnClose(wxCloseEvent &p_event) -{ - if (!m_runner || !m_stopButton->IsEnabled()) { - p_event.Skip(); - return; - } - - m_stopRequested = true; - m_stopButton->Enable(false); - m_runner->RequestCancel(); - - if (p_event.CanVeto()) { - p_event.Veto(); - } - else { - p_event.Skip(); - } -} - -void LogitBehavDialog::OnSave(wxCommandEvent &) -{ - wxFileDialog dialog(this, _("Choose file"), wxT(""), wxT(""), - wxT("CSV files (*.csv)|*.csv|") wxT("All files (*.*)|*.*"), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (dialog.ShowModal() == wxID_OK) { - std::ofstream file(dialog.GetPath().mb_str()); - file << static_cast(m_output.mb_str()); - } -} - -} // namespace Gambit::GUI diff --git a/src/gui/dlefglogit.h b/src/gui/dlefglogit.h deleted file mode 100644 index 727463dedb..0000000000 --- a/src/gui/dlefglogit.h +++ /dev/null @@ -1,56 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/gui/dlefglogit.h -// Dialog for monitoring logit equilibrium computation -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#ifndef GAMBIT_GUI_DLEFGLOGIT_H -#define GAMBIT_GUI_DLEFGLOGIT_H - -#include "gamedoc.h" - -namespace Gambit::GUI { -class LogitBehavGrid; -class LogitBehavThreadRunner; - -class LogitBehavDialog final : public wxDialog { - std::shared_ptr m_doc; - std::unique_ptr m_runner; - LogitBehavGrid *m_behavList; - wxStaticText *m_statusText; - wxButton *m_stopButton, *m_okButton, *m_saveButton; - wxString m_output; - bool m_stopRequested{false}; - - void OnStop(wxCommandEvent &); - void OnClose(wxCloseEvent &); - void OnRunnerPoint(wxThreadEvent &); - void OnRunnerFinished(wxThreadEvent &); - void OnSave(wxCommandEvent &); - - void Start(); - -public: - LogitBehavDialog(wxWindow *p_parent, const std::shared_ptr &p_doc); - ~LogitBehavDialog() override; -}; - -} // namespace Gambit::GUI - -#endif // GAMBIT_GUI_DLEFGLOGIT_H diff --git a/src/gui/dlefgreveal.cc b/src/gui/dlefgreveal.cc index 4554d0c896..e4a517f045 100644 --- a/src/gui/dlefgreveal.cc +++ b/src/gui/dlefgreveal.cc @@ -60,10 +60,10 @@ RevealMoveDialog::RevealMoveDialog(wxWindow *p_parent, const Game &p_game) auto f = groupLabel->GetFont(); f.SetWeight(wxFONTWEIGHT_BOLD); groupLabel->SetFont(f); - topSizer->Add(groupLabel, wxSizerFlags().Border(wxLEFT | wxTOP | wxRIGHT, 10)); + topSizer->Add(groupLabel, wxSizerFlags().Border(wxLEFT | wxTOP | wxRIGHT, FromDIP(10))); auto *playerBox = new wxBoxSizer(wxVERTICAL); - playerBox->AddSpacer(3); + playerBox->AddSpacer(FromDIP(3)); const auto &players = p_game->GetPlayers(); m_entries.reserve(players.size()); @@ -80,14 +80,14 @@ RevealMoveDialog::RevealMoveDialog(wxWindow *p_parent, const Game &p_game) cb->SetValue(true); cb->Bind(wxEVT_CHECKBOX, &RevealMoveDialog::OnCheckbox, this); m_entries.push_back({player, cb}); - playerBox->Add(cb, wxSizerFlags().Expand().Border(wxLEFT | wxRIGHT | wxTOP, 4)); + playerBox->Add(cb, wxSizerFlags().Expand().Border(wxLEFT | wxRIGHT | wxTOP, FromDIP(4))); } - topSizer->Add(playerBox, wxSizerFlags(1).Expand().Border(wxALL, 5)); + topSizer->Add(playerBox, wxSizerFlags(1).Expand().Border(wxALL, FromDIP(5))); auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL); buttonSizer->Realize(); - topSizer->Add(buttonSizer, wxSizerFlags().Right().Border(wxALL, 10)); + topSizer->Add(buttonSizer, wxSizerFlags().Right().Border(wxALL, FromDIP(10))); SetSizerAndFit(topSizer); CenterOnParent(); diff --git a/src/gui/dlgameprop.cc b/src/gui/dlgameprop.cc index 240ad3e88c..4a49262459 100644 --- a/src/gui/dlgameprop.cc +++ b/src/gui/dlgameprop.cc @@ -268,6 +268,7 @@ void PlayerPanel::Rebuild() const int activeCount = ActiveCount(); const wxSize buttonSize(FromDIP(28), -1); + const int buttonGap = FromDIP(2); for (size_t i = 0; i < m_rows.size(); i++) { Row &row = m_rows[i]; @@ -318,14 +319,14 @@ void PlayerPanel::Rebuild() row.upButton->Enable(i > 0); row.upButton->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent &) { MovePlayerUp(static_cast(i)); }); - buttonSizer->Add(row.upButton, 0, wxLEFT, 2); + buttonSizer->Add(row.upButton, 0, wxLEFT, buttonGap); row.downButton = new wxButton(this, wxID_ANY, wxUniChar(0x2193), wxDefaultPosition, buttonSize); row.downButton->Enable(i + 1 < m_rows.size()); row.downButton->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent &) { MovePlayerDown(static_cast(i)); }); - buttonSizer->Add(row.downButton, 0, wxLEFT, 2); + buttonSizer->Add(row.downButton, 0, wxLEFT, buttonGap); } else { // Keep the delete/restore button aligned under its counterpart in other rows, in place @@ -334,8 +335,8 @@ void PlayerPanel::Rebuild() // them dangling. row.upButton = nullptr; row.downButton = nullptr; - buttonSizer->AddSpacer(buttonSize.GetWidth() + 2); - buttonSizer->AddSpacer(buttonSize.GetWidth() + 2); + buttonSizer->AddSpacer(buttonSize.GetWidth() + buttonGap); + buttonSizer->AddSpacer(buttonSize.GetWidth() + buttonGap); } const wxString blockedReason = row.isDeleted ? wxString() : BlockedDeleteReason(row); @@ -359,7 +360,7 @@ void PlayerPanel::Rebuild() row.deleteButton->Enable(canDelete); row.deleteButton->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent &) { ToggleDeleted(static_cast(i)); }); - buttonSizer->Add(row.deleteButton, 0, wxLEFT, 2); + buttonSizer->Add(row.deleteButton, 0, wxLEFT, buttonGap); gridSizer->Add(buttonSizer, 0, wxALIGN_CENTER_VERTICAL); } @@ -374,10 +375,10 @@ void PlayerPanel::Rebuild() addButton->SetToolTip(_("Add player")); addButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { AddPlayer(); }); auto *addSizer = new wxBoxSizer(wxHORIZONTAL); - addSizer->Add(addButton, 0, wxLEFT, 2); + addSizer->Add(addButton, 0, wxLEFT, buttonGap); gridSizer->Add(addSizer, 0, wxALIGN_CENTER_VERTICAL); - m_topSizer->Add(gridSizer, 1, wxALL | wxEXPAND, 5); + m_topSizer->Add(gridSizer, 1, wxALL | wxEXPAND, FromDIP(5)); FitInside(); Layout(); m_rebuilding = false; @@ -553,6 +554,8 @@ GamePropertiesDialog::GamePropertiesDialog(wxWindow *p_parent, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), m_doc(p_doc) { + const int S = FromDIP(5); + auto *topSizer = new wxBoxSizer(wxVERTICAL); auto *notebook = new wxNotebook(this, wxID_ANY); @@ -562,20 +565,20 @@ GamePropertiesDialog::GamePropertiesDialog(wxWindow *p_parent, auto *titleSizer = new wxBoxSizer(wxHORIZONTAL); titleSizer->Add(new wxStaticText(generalPanel, wxID_STATIC, _("Title")), 0, - wxALL | wxALIGN_CENTER, 5); + wxALL | wxALIGN_CENTER, S); m_title = new wxTextCtrl(generalPanel, wxID_ANY, wxString::FromUTF8(m_doc->GetGame()->GetTitle()), - wxDefaultPosition, wxSize(400, -1)); - titleSizer->Add(m_title, 1, wxALL | wxALIGN_CENTER, 5); + wxDefaultPosition, wxSize(FromDIP(400), -1)); + titleSizer->Add(m_title, 1, wxALL | wxALIGN_CENTER, S); generalSizer->Add(titleSizer, 0, wxALL | wxEXPAND, 0); auto *commentSizer = new wxBoxSizer(wxHORIZONTAL); commentSizer->Add(new wxStaticText(generalPanel, wxID_STATIC, _("Comment")), 0, - wxALL | wxALIGN_CENTER, 5); + wxALL | wxALIGN_CENTER, S); m_comment = new wxTextCtrl(generalPanel, wxID_ANY, wxString::FromUTF8(m_doc->GetGame()->GetDescription()), - wxDefaultPosition, wxSize(400, -1), wxTE_MULTILINE); - commentSizer->Add(m_comment, 1, wxALL | wxALIGN_CENTER, 5); + wxDefaultPosition, wxSize(FromDIP(400), -1), wxTE_MULTILINE); + commentSizer->Add(m_comment, 1, wxALL | wxALIGN_CENTER, S); generalSizer->Add(commentSizer, 1, wxALL | wxEXPAND, 0); auto *boxSizer = @@ -583,33 +586,33 @@ GamePropertiesDialog::GamePropertiesDialog(wxWindow *p_parent, boxSizer->Add(new wxStaticText(generalPanel, wxID_STATIC, wxString(_("Filename: ")) + m_doc->GetFilename()), - 0, wxALL, 5); + 0, wxALL, S); const Game game = m_doc->GetGame(); if (game->IsConstSum()) { boxSizer->Add(new wxStaticText(generalPanel, wxID_STATIC, _("This is a constant-sum game")), 0, - wxALL, 5); + wxALL, S); } else { boxSizer->Add( new wxStaticText(generalPanel, wxID_STATIC, _("This is not a constant-sum game")), 0, - wxALL, 5); + wxALL, S); } if (game->IsTree()) { if (game->IsPerfectRecall()) { boxSizer->Add( new wxStaticText(generalPanel, wxID_STATIC, _("This is a game of perfect recall")), 0, - wxALL, 5); + wxALL, S); } else { boxSizer->Add( new wxStaticText(generalPanel, wxID_STATIC, _("This is not a game of perfect recall")), - 0, wxALL, 5); + 0, wxALL, S); } } - generalSizer->Add(boxSizer, 0, wxALL | wxEXPAND, 5); + generalSizer->Add(boxSizer, 0, wxALL | wxEXPAND, S); generalPanel->SetSizer(generalSizer); notebook->AddPage(generalPanel, _("General")); @@ -633,25 +636,20 @@ GamePropertiesDialog::GamePropertiesDialog(wxWindow *p_parent, playersPanel->SetSizer(playersSizer); notebook->AddPage(playersPanel, _("Players")); - topSizer->Add(notebook, 1, wxALL | wxEXPAND, 5); + topSizer->Add(notebook, 1, wxALL | wxEXPAND, S); m_errorText = new wxStaticText(this, wxID_STATIC, wxEmptyString); m_errorText->SetForegroundColour(*wxRED); - topSizer->Add(m_errorText, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 5); - - auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); - buttonSizer->Add(new wxButton(this, wxID_CANCEL, _("Cancel")), 0, wxALL, 5); - auto *okButton = new wxButton(this, wxID_OK, _("OK")); - okButton->SetDefault(); - buttonSizer->Add(okButton, 0, wxALL, 5); + topSizer->Add(m_errorText, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, S); - topSizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); + if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { + topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); + } SetSizer(topSizer); - topSizer->Fit(this); topSizer->SetSizeHints(this); - - wxTopLevelWindowBase::Layout(); + SetSize(GetBestSize()); + SetMinSize(GetSize()); CenterOnParent(); UpdateValidation(); diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index b5c56336f9..47c2a47a0b 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -298,8 +298,12 @@ wxString ParameterDescription(const NashMethodSpec &p_method) } // namespace NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxDialog(p_parent, wxID_ANY, wxT("Compute Nash equilibria"), wxDefaultPosition), m_doc(p_doc) + : wxDialog(p_parent, wxID_ANY, wxT("Compute Nash equilibria"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_doc(p_doc) { + const int S = FromDIP(5); + auto *topSizer = new wxBoxSizer(wxVERTICAL); if (m_doc->GetGame()->NumPlayers() == 2) { @@ -319,7 +323,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrGetId(), wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler(NashChoiceDialog::OnCount)); - topSizer->Add(m_countChoice, 0, wxALL | wxEXPAND, 5); + topSizer->Add(m_countChoice, 0, wxALL | wxEXPAND, S); if (p_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { wxString methodChoices[] = {s_recommended, s_lp, s_simpdiv, s_logit, s_enumpoly}; @@ -332,13 +336,13 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrSetSelection(0); - topSizer->Add(m_methodChoice, 0, wxALL | wxEXPAND, 5); + topSizer->Add(m_methodChoice, 0, wxALL | wxEXPAND, S); if (m_doc->GetGame()->IsTree()) { wxString repChoices[] = {wxT("using the extensive game"), wxT("using the strategic game")}; m_repChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, repChoices); m_repChoice->SetSelection(0); - topSizer->Add(m_repChoice, 0, wxALL | wxEXPAND, 5); + topSizer->Add(m_repChoice, 0, wxALL | wxEXPAND, S); // We only need to respond to changes in method when we have an // extensive game @@ -351,12 +355,9 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(new wxButton(this, wxID_CANCEL, _("Cancel")), 0, wxALL, 5); - auto *okButton = new wxButton(this, wxID_OK, _("OK")); - okButton->SetDefault(); - buttonSizer->Add(okButton, 0, wxALL, 5); - topSizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); + if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { + topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); + } SetSizer(topSizer); topSizer->Fit(this); diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 91746bf96a..07ade61420 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -361,25 +361,28 @@ class NashMonitorDialog final : public wxDialog { NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, const std::shared_ptr &p_doc, const std::shared_ptr &p_command) - : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition), + : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), m_doc(p_doc), m_output(p_command) { + const int S = FromDIP(5); + auto *sizer = new wxBoxSizer(wxVERTICAL); auto *startSizer = new wxBoxSizer(wxHORIZONTAL); m_statusText = new wxStaticText(this, wxID_STATIC, "The computation is currently in progress."); - startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, 5); + startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, S); m_countText = new wxStaticText(this, wxID_STATIC, wxT("Number of equilibria found so far: 0 ")); - startSizer->Add(m_countText, 0, wxALL | wxALIGN_CENTER, 5); + startSizer->Add(m_countText, 0, wxALL | wxALIGN_CENTER, S); m_stopButton = new wxBitmapButton(this, wxID_ANY, wxBitmap(stop_xpm)); m_stopButton->Enable(false); m_stopButton->SetToolTip(_("Stop the computation")); - startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, 5); + startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, S); - sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, 5); + sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, S); if (p_command->IsBehavior()) { m_profileList = new MixedBehaviorProfileList(this, m_doc); @@ -387,16 +390,18 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, else { m_profileList = new MixedStrategyProfileList(this, m_doc); } - m_profileList->SetMinSize(wxSize(500, 300)); - sizer->Add(m_profileList, 1, wxALL | wxEXPAND, 5); + m_profileList->SetMinSize(wxSize(FromDIP(500), FromDIP(300))); + sizer->Add(m_profileList, 1, wxALL | wxEXPAND, S); auto *buttonSizer = CreateStdDialogButtonSizer(wxOK); - sizer->Add(buttonSizer, 0, wxALL | wxEXPAND, 5); + sizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); m_okButton = dynamic_cast(FindWindow(wxID_OK)); m_okButton->Enable(false); - SetSizerAndFit(sizer); + SetSizer(sizer); sizer->SetSizeHints(this); + SetSize(GetBestSize()); + SetMinSize(GetSize()); CenterOnParent(); Bind(wxEVT_EXTERNAL_RUNNER_PROFILE, &NashMonitorDialog::OnRunnerProfile, this); diff --git a/src/gui/dlnewtable.cc b/src/gui/dlnewtable.cc index f767874669..f2f5aadea8 100644 --- a/src/gui/dlnewtable.cc +++ b/src/gui/dlnewtable.cc @@ -58,7 +58,7 @@ void NewTableDialog::LayoutControls() { m_topSizer = new wxBoxSizer(wxVERTICAL); - auto *formSizer = new wxFlexGridSizer(2, 2, 8, 12); + auto *formSizer = new wxFlexGridSizer(2, 2, FromDIP(8), FromDIP(12)); formSizer->AddGrowableCol(1, 1); auto *playerCountLabel = FindWindowByName("playerCountLabel", this); @@ -66,13 +66,13 @@ void NewTableDialog::LayoutControls() formSizer->Add(m_playerCountCtrl, 0); auto *strategyBox = new wxStaticBoxSizer(wxVERTICAL, this, wxT("Strategies per player")); - strategyBox->Add(m_strategyRowsSizer, 0, wxEXPAND | wxALL, 8); + strategyBox->Add(m_strategyRowsSizer, 0, wxEXPAND | wxALL, FromDIP(8)); auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL); - m_topSizer->Add(formSizer, 0, wxEXPAND | wxALL, 12); - m_topSizer->Add(strategyBox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 12); - m_topSizer->Add(buttonSizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 12); + m_topSizer->Add(formSizer, 0, wxEXPAND | wxALL, FromDIP(12)); + m_topSizer->Add(strategyBox, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); + m_topSizer->Add(buttonSizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); SetSizer(m_topSizer); } @@ -93,7 +93,7 @@ void NewTableDialog::RebuildStrategyControls() const int playerCount = m_playerCountCtrl->GetValue(); - auto *grid = new wxFlexGridSizer(playerCount, 2, 8, 12); + auto *grid = new wxFlexGridSizer(playerCount, 2, FromDIP(8), FromDIP(12)); grid->AddGrowableCol(1, 1); for (int i = 0; i < playerCount; ++i) { diff --git a/src/gui/dlnfglogit.cc b/src/gui/dlnfglogit.cc deleted file mode 100644 index 51d1e314bf..0000000000 --- a/src/gui/dlnfglogit.cc +++ /dev/null @@ -1,420 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/gui/dlnfglogit.cc -// Dialog for monitoring progress of strategic-form logit equilibrium computation -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#include -#include - -#include -#ifndef WX_PRECOMP -#include -#endif // WX_PRECOMP -#include - -#include -#include "dlnfglogit.h" -#include "solvers/logit/logit.h" - -namespace Gambit::GUI { - -//========================================================================= -// class LogitMixedTable -//========================================================================= - -//! -//! Data model for the logit correspondence display: one row per -//! computed profile, received incrementally from the solver thread. -//! Row growth is deliberately batched (see AddProfile) so the grid -//! isn't resized/repainted on every single profile received. -//! -class LogitMixedTable final : public wxGridTableBase { - std::shared_ptr m_doc; - Array m_lambdas; - Array>> m_profiles; - int m_numCols{0}; - int m_numRows{0}; - -public: - explicit LogitMixedTable(const std::shared_ptr &p_doc) : m_doc(p_doc) {} - - int GetNumberRows() override { return m_numRows; } - int GetNumberCols() override { return m_numCols; } - bool IsEmptyCell(int, int) override { return false; } - wxString GetValue(int row, int col) override; - void SetValue(int, int, const wxString &) override {} - wxGridCellAttr *GetAttr(int row, int col, wxGridCellAttr::wxAttrKind kind) override; - - //! - //! wxGrid::SetColLabelValue/SetCornerLabelValue on the grid just forward - //! to the table's own (default no-op) methods of the same name, so - //! headers must be supplied by overriding the getters here instead. - //! - wxString GetColLabelValue(int col) override; - wxString GetCornerLabelValue() const override { return wxT("#"); } - - void AddProfile(const LogitQREMixedStrategyProfile &p_qre, bool p_forceShow); -}; - -wxString LogitMixedTable::GetColLabelValue(int col) -{ - if (col == 0) { - return wxT("Lambda"); - } - - const GameStrategy strategy = m_doc->GetGame()->GetStrategy(col); - const GamePlayer player = strategy->GetPlayer(); - wxString label; - label << player->GetNumber() << ": " << strategy->GetLabel(); - return label; -} - -wxString LogitMixedTable::GetValue(int row, int col) -{ - if (col == 0) { - return {lexical_cast(m_lambdas[row + 1], m_doc->GetStyle().NumDecimals()).c_str(), - *wxConvCurrent}; - } - - const auto profile = m_profiles[row + 1]; - return {lexical_cast((*profile)[col], m_doc->GetStyle().NumDecimals()).c_str(), - *wxConvCurrent}; -} - -wxGridCellAttr *LogitMixedTable::GetAttr(int row, int col, wxGridCellAttr::wxAttrKind) -{ - auto *attr = new wxGridCellAttr(); - attr->SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL)); - attr->SetAlignment(wxALIGN_RIGHT, wxALIGN_CENTRE); - attr->SetReadOnly(true); - - if (col > 0) { - const GameStrategy strategy = m_doc->GetGame()->GetStrategy(col); - const GamePlayer player = strategy->GetPlayer(); - - attr->SetTextColour(m_doc->GetStyle().GetPlayerColor(player)); - attr->SetBackgroundColour(player->GetNumber() % 2 == 0 ? wxColour(250, 250, 250) - : wxColour(225, 225, 225)); - } - else { - attr->SetTextColour(*wxBLACK); - attr->SetBackgroundColour(wxColour(250, 250, 250)); - } - - return attr; -} - -void LogitMixedTable::AddProfile(const LogitQREMixedStrategyProfile &p_qre, bool p_forceShow) -{ - wxGrid *view = GetView(); - - if (m_numCols == 0) { - m_numCols = static_cast(m_doc->GetGame()->GetStrategies().size()) + 1; - if (view) { - wxGridTableMessage msg(this, wxGRIDTABLE_NOTIFY_COLS_APPENDED, m_numCols); - view->ProcessTableMessage(msg); - } - } - - auto profile = std::make_shared>( - m_doc->GetGame()->NewMixedStrategyProfile(0.0)); - m_lambdas.push_back(p_qre.GetLambda()); - for (size_t i = 1; i <= profile->MixedProfileLength(); i++) { - (*profile)[i] = p_qre[i]; - } - m_profiles.push_back(profile); - - if (p_forceShow || static_cast(m_profiles.size()) - m_numRows > 20) { - const int newRows = static_cast(m_profiles.size()) - m_numRows; - m_numRows = static_cast(m_profiles.size()); - if (view) { - wxGridTableMessage msg(this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, newRows); - view->ProcessTableMessage(msg); - view->MakeCellVisible(m_numRows - 1, 0); - } - } - - // Lambda tends to get large, so this column usually needs resized - if (view) { - view->AutoSizeColumn(0); - } -} - -//========================================================================= -// class LogitMixedGrid -//========================================================================= - -class LogitMixedGrid final : public wxGrid { - LogitMixedTable *m_gridTable; - - //! - //! @name Overriding wxGrid behavior to disable selection & the cursor - //! highlight rectangle, matching the read-only, non-interactive table - //! this replaces. - //! - //@{ - void OnRangeSelecting(wxGridRangeSelectEvent &p_event) { p_event.Veto(); } - void OnSelectCell(wxGridEvent &p_event) { p_event.Skip(); } - //@} - -public: - LogitMixedGrid(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxGrid(p_parent, wxID_ANY), m_gridTable(new LogitMixedTable(p_doc)) - { - SetTable(m_gridTable, true); - EnableEditing(false); - SetRowLabelSize(40); - SetColLabelSize(25); - - SetCellHighlightPenWidth(0); - SetCellHighlightROPenWidth(0); - - Bind(wxEVT_GRID_RANGE_SELECTING, &LogitMixedGrid::OnRangeSelecting, this); - Bind(wxEVT_GRID_SELECT_CELL, &LogitMixedGrid::OnSelectCell, this); - } - - void AddProfile(const LogitQREMixedStrategyProfile &p_qre, bool p_forceShow) - { - m_gridTable->AddProfile(p_qre, p_forceShow); - } -}; - -namespace { - -wxDECLARE_EVENT(wxEVT_LOGIT_POINT, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_LOGIT_POINT, wxThreadEvent); - -wxDECLARE_EVENT(wxEVT_LOGIT_FINISHED, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_LOGIT_FINISHED, wxThreadEvent); - -#include "bitmaps/stop.xpm" - -} // namespace - -// -// Traces the logit equilibrium correspondence directly, in-process, on a -// worker thread, rather than shelling out to gambit-logit and parsing its -// output. Posts each point on the branch back to the dialog as it is found. -// -class LogitMixedThreadRunner final : public wxThread { - wxEvtHandler *m_parent; - Game m_game; - CancelToken m_cancel; - - void PostPoint(const LogitQREMixedStrategyProfile &p_qre) const - { - auto *event = new wxThreadEvent(wxEVT_LOGIT_POINT); - event->SetPayload(p_qre); - wxQueueEvent(m_parent, event); - } - - ExitCode Entry() override - { - int exitCode = 0; - try { - const LogitQREMixedStrategyProfile start(m_game); - LogitStrategySolve( - start, 1.0e-8, 1.0, 0.03, 1.1, Nash::NullStrategyCallback, - [this](const LogitEvent &p_event) { - PostPoint(std::get>(p_event).qre); - }, - m_cancel); - } - catch (const ComputationCanceledException &) { - // Not an error -- LogitMixedDialog's m_stopRequested flag (already set - // before RequestCancel() was called) is what determines that the - // "finished" event below is reported as a stop rather than a failure. - } - catch (const std::exception &) { - exitCode = 1; - } - - auto *evt = new wxThreadEvent(wxEVT_LOGIT_FINISHED); - evt->SetInt(exitCode); - wxQueueEvent(m_parent, evt); - return static_cast(0); - } - -public: - LogitMixedThreadRunner(wxEvtHandler *p_parent, Game p_game) - : wxThread(wxTHREAD_JOINABLE), m_parent(p_parent), m_game(std::move(p_game)) - { - } - - void RequestCancel() { m_cancel.RequestCancel(); } -}; - -LogitMixedDialog::LogitMixedDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxDialog(p_parent, wxID_ANY, wxT("Compute quantal response equilibria"), wxDefaultPosition), - m_doc(p_doc), m_mixedList(new LogitMixedGrid(this, m_doc)) -{ - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *startSizer = new wxBoxSizer(wxHORIZONTAL); - - m_statusText = - new wxStaticText(this, wxID_STATIC, wxT("The computation is currently in progress.")); - m_statusText->SetForegroundColour(*wxBLUE); - startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, 5); - - m_stopButton = new wxBitmapButton(this, wxID_CANCEL, wxBitmap(stop_xpm)); - m_stopButton->SetToolTip(_("Stop the computation")); - startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, 5); - m_stopButton->Bind(wxEVT_BUTTON, &LogitMixedDialog::OnStop, this); - - sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, 5); - - m_mixedList->SetSizeHints(wxSize(600, 400)); - sizer->Add(m_mixedList, 0, wxALL | wxALIGN_CENTER, 5); - - auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); - - m_saveButton = new wxButton(this, wxID_SAVE, wxT("Save correspondence to .csv file")); - m_saveButton->Enable(false); - buttonSizer->Add(m_saveButton, 0, wxALL | wxALIGN_CENTER, 5); - m_saveButton->Bind(wxEVT_BUTTON, &LogitMixedDialog::OnSave, this); - - m_okButton = new wxButton(this, wxID_OK, wxT("OK")); - buttonSizer->Add(m_okButton, 0, wxALL | wxALIGN_CENTER, 5); - m_okButton->Enable(false); - - sizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5); - - SetSizer(sizer); - sizer->Fit(this); - sizer->SetSizeHints(this); - wxTopLevelWindowBase::Layout(); - CenterOnParent(); - - Bind(wxEVT_LOGIT_POINT, &LogitMixedDialog::OnRunnerPoint, this); - Bind(wxEVT_LOGIT_FINISHED, &LogitMixedDialog::OnRunnerFinished, this); - Bind(wxEVT_CLOSE_WINDOW, &LogitMixedDialog::OnClose, this); - - Start(); -} - -LogitMixedDialog::~LogitMixedDialog() = default; - -void LogitMixedDialog::Start() -{ - m_doc->BuildNfg(); - // GameRep's lazy caches are not synchronized against concurrent first - // touch from two threads, so force the strategy derivation the worker - // will need on this (the main) thread before handing the game over. - m_doc->GetGame()->GetStrategies(); - - m_runner = std::make_unique(this, m_doc->GetGame()); - if (m_runner->Run() != wxTHREAD_NO_ERROR) { - m_runner.reset(); - m_statusText->SetLabel(wxT("Failed to launch computation.")); - m_statusText->SetForegroundColour(*wxRED); - m_okButton->Enable(true); - } -} - -void LogitMixedDialog::OnRunnerPoint(wxThreadEvent &p_event) -{ - const auto &qre = p_event.GetPayload(); - m_mixedList->AddProfile(qre, false); - - const int decimals = m_doc->GetStyle().NumDecimals(); - wxString line(lexical_cast(qre.GetLambda(), decimals).c_str(), *wxConvCurrent); - for (size_t i = 1; i <= qre.size(); i++) { - line += - wxT(",") + wxString(lexical_cast(qre[i], decimals).c_str(), *wxConvCurrent); - } - m_output += line + wxT("\n"); -} - -void LogitMixedDialog::OnRunnerFinished(wxThreadEvent &p_event) -{ - m_stopButton->Enable(false); - - if (m_runner) { - // Joinable thread: must be waited on before it can be safely destroyed. - // Entry() has already posted this event and is returning, so this - // should not block for any appreciable time. - m_runner->Wait(); - m_runner.reset(); - } - - if (m_stopRequested) { - m_statusText->SetLabel(wxT("The computation has been stopped.")); - m_statusText->SetForegroundColour(wxColour(196, 128, 0)); - } - else if (p_event.GetInt() == 0) { - m_statusText->SetLabel(wxT("The computation has completed.")); - m_statusText->SetForegroundColour(wxColour(0, 192, 0)); - } - else { - m_statusText->SetLabel(wxT("The computation ended abnormally.")); - m_statusText->SetForegroundColour(*wxRED); - } - - m_okButton->Enable(true); - m_saveButton->Enable(true); -} - -void LogitMixedDialog::OnStop(wxCommandEvent &) -{ - m_stopRequested = true; - m_stopButton->Enable(false); - if (m_runner) { - m_runner->RequestCancel(); - } -} - -void LogitMixedDialog::OnClose(wxCloseEvent &p_event) -{ - if (!m_runner || !m_stopButton->IsEnabled()) { - p_event.Skip(); - return; - } - - m_stopRequested = true; - m_stopButton->Enable(false); - m_runner->RequestCancel(); - - if (p_event.CanVeto()) { - p_event.Veto(); - } - else { - p_event.Skip(); - } -} - -void LogitMixedDialog::OnSave(wxCommandEvent &) -{ - wxFileDialog dialog(this, _("Choose file"), wxT(""), wxT(""), - wxT("CSV files (*.csv)|*.csv|") wxT("All files (*.*)|*.*"), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (dialog.ShowModal() == wxID_OK) { - std::ofstream file(dialog.GetPath().mb_str()); - file << static_cast(m_output.mb_str()); - } -} - -void LogitStrategic(wxWindow *p_parent, const std::shared_ptr &p_doc) -{ - LogitMixedDialog(p_parent, p_doc).ShowModal(); -} - -} // namespace Gambit::GUI diff --git a/src/gui/dlnfglogit.h b/src/gui/dlnfglogit.h deleted file mode 100644 index 28788e1f83..0000000000 --- a/src/gui/dlnfglogit.h +++ /dev/null @@ -1,58 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/gui/dlnfglogit.h -// Dialog for monitoring logit equilibrium computation -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#ifndef GAMBIT_GUI_DLNFGLOGIT_H -#define GAMBIT_GUI_DLNFGLOGIT_H - -#include "gamedoc.h" - -namespace Gambit::GUI { -class LogitMixedGrid; -class LogitMixedThreadRunner; - -class LogitMixedDialog final : public wxDialog { - std::shared_ptr m_doc; - std::unique_ptr m_runner; - LogitMixedGrid *m_mixedList; - wxStaticText *m_statusText; - wxButton *m_stopButton, *m_okButton, *m_saveButton; - wxString m_output; - bool m_stopRequested{false}; - - void OnStop(wxCommandEvent &); - void OnClose(wxCloseEvent &); - void OnRunnerPoint(wxThreadEvent &); - void OnRunnerFinished(wxThreadEvent &); - void OnSave(wxCommandEvent &); - - void Start(); - -public: - LogitMixedDialog(wxWindow *p_parent, const std::shared_ptr &p_doc); - ~LogitMixedDialog() override; -}; - -void LogitStrategic(wxWindow *p_parent, const std::shared_ptr &p_doc); - -} // namespace Gambit::GUI - -#endif // GAMBIT_GUI_DLNFGLOGIT_H diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index 38507710bb..1e2120237e 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -53,7 +53,7 @@ #include "dlexcept.h" #include "dlgameprop.h" #include "dlnash.h" -#include "dlefglogit.h" +#include "dllogit.h" #include "dlabout.h" #include "dlefglayout.h" @@ -1085,8 +1085,6 @@ void GameFrame::OnToolsEquilibrium(wxCommandEvent &) } } -extern void LogitStrategic(wxWindow *, const std::shared_ptr &); - void GameFrame::OnToolsQre(wxCommandEvent &) { if (!m_doc->GetGame()->IsPerfectRecall()) { From 1a5a0b9375f343361ea0a415f1c0a1a6493f1009 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 27 Aug 2026 07:42:31 +0100 Subject: [PATCH 2/2] Adding missing files for new unified logit dialog --- src/gui/dllogit.cc | 32 +++ src/gui/dllogit.h | 499 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 531 insertions(+) create mode 100644 src/gui/dllogit.cc create mode 100644 src/gui/dllogit.h diff --git a/src/gui/dllogit.cc b/src/gui/dllogit.cc new file mode 100644 index 0000000000..a7d4d1c01a --- /dev/null +++ b/src/gui/dllogit.cc @@ -0,0 +1,32 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/dllogit.cc +// Dialog for monitoring progress of logit equilibrium computation +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "dllogit.h" + +namespace Gambit::GUI { + +void LogitStrategic(wxWindow *p_parent, const std::shared_ptr &p_doc) +{ + LogitMixedDialog(p_parent, p_doc).ShowModal(); +} + +} // namespace Gambit::GUI diff --git a/src/gui/dllogit.h b/src/gui/dllogit.h new file mode 100644 index 0000000000..7b2198937d --- /dev/null +++ b/src/gui/dllogit.h @@ -0,0 +1,499 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/dllogit.h +// Dialog for monitoring progress of logit equilibrium computation +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef GAMBIT_GUI_DLLOGIT_H +#define GAMBIT_GUI_DLLOGIT_H + +#include +#include + +#include +#ifndef WX_PRECOMP +#include +#endif // WX_PRECOMP +#include +#include + +#include "gamedoc.h" +#include "solvers/logit/logit.h" + +namespace Gambit::GUI { + +namespace { +wxDECLARE_EVENT(wxEVT_LOGIT_POINT, wxThreadEvent); +wxDEFINE_EVENT(wxEVT_LOGIT_POINT, wxThreadEvent); + +wxDECLARE_EVENT(wxEVT_LOGIT_FINISHED, wxThreadEvent); +wxDEFINE_EVENT(wxEVT_LOGIT_FINISHED, wxThreadEvent); + +#include "bitmaps/stop.xpm" +} // namespace + +//! +//! Traits distinguishing the extensive-form (behavior) and strategic-form +//! (mixed) instantiations of the logit correspondence dialog: which profile +//! and QRE types to trace, how a column maps to a player/label/shading +//! group, how to prepare the game for tracing, and which solver to run. +//! +struct BehavLogitTraits { + using ProfileType = MixedBehaviorProfile; + using QREType = LogitQRE; + + static wxString ColumnLabel(const std::shared_ptr &p_doc, int p_col) + { + const GameAction action = p_doc->GetAction(p_col); + wxString label; + label << action->GetInfoset()->GetNumber() << ": " << action->GetLabel(); + return label; + } + static GamePlayer ColumnPlayer(const std::shared_ptr &p_doc, int p_col) + { + return p_doc->GetAction(p_col)->GetInfoset()->GetPlayer(); + } + static int ColumnGroup(const std::shared_ptr &p_doc, int p_col) + { + return p_doc->GetAction(p_col)->GetInfoset()->GetNumber(); + } + // GameRep's lazy caches are not synchronized against concurrent first touch from two + // threads, so force the sequence-form derivation the worker will need on this (the main) + // thread before handing the game over. Constructing a profile forces + // EnsureInfosetOrdering() via the same public code path the worker's own starting profile + // will use. + static void PrepareGame(const std::shared_ptr &p_doc) + { + const MixedBehaviorProfile warmup(p_doc->GetGame()); + p_doc->GetGame()->EnsureSequences(); + } + static void Solve(const Game &p_game, const LogitEventCallbackType &p_onEvent, + const CancelToken &p_cancel) + { + const QREType start(p_game); + LogitBehaviorSolve(start, 1.0e-8, 1.0, 0.03, 1.1, Nash::NullBehaviorCallback, + p_onEvent, p_cancel); + } +}; + +struct MixedLogitTraits { + using ProfileType = MixedStrategyProfile; + using QREType = LogitQRE; + + static wxString ColumnLabel(const std::shared_ptr &p_doc, int p_col) + { + const GameStrategy strategy = p_doc->GetGame()->GetStrategy(p_col); + wxString label; + label << strategy->GetPlayer()->GetNumber() << ": " << strategy->GetLabel(); + return label; + } + static GamePlayer ColumnPlayer(const std::shared_ptr &p_doc, int p_col) + { + return p_doc->GetGame()->GetStrategy(p_col)->GetPlayer(); + } + static int ColumnGroup(const std::shared_ptr &p_doc, int p_col) + { + return p_doc->GetGame()->GetStrategy(p_col)->GetPlayer()->GetNumber(); + } + // GameRep's lazy caches are not synchronized against concurrent first touch from two + // threads, so force the strategy derivation the worker will need on this (the main) thread + // before handing the game over. + static void PrepareGame(const std::shared_ptr &p_doc) + { + p_doc->BuildNfg(); + p_doc->GetGame()->GetStrategies(); + } + static void Solve(const Game &p_game, const LogitEventCallbackType &p_onEvent, + const CancelToken &p_cancel) + { + const QREType start(p_game); + LogitStrategySolve(start, 1.0e-8, 1.0, 0.03, 1.1, Nash::NullStrategyCallback, + p_onEvent, p_cancel); + } +}; + +//========================================================================= +// class LogitTable +//========================================================================= + +//! +//! Data model for the logit correspondence display: one row per +//! computed profile, received incrementally from the solver thread. +//! Row growth is deliberately batched (see AddProfile) so the grid +//! isn't resized/repainted on every single profile received. +//! +template class LogitTable final : public wxGridTableBase { + std::shared_ptr m_doc; + Array m_lambdas; + Array> m_profiles; + int m_numCols{0}; + int m_numRows{0}; + +public: + explicit LogitTable(const std::shared_ptr &p_doc) : m_doc(p_doc) {} + + int GetNumberRows() override { return m_numRows; } + int GetNumberCols() override { return m_numCols; } + bool IsEmptyCell(int, int) override { return false; } + + wxString GetValue(int row, int col) override + { + if (col == 0) { + return { + lexical_cast(m_lambdas[row + 1], m_doc->GetStyle().NumDecimals()).c_str(), + *wxConvCurrent}; + } + const auto profile = m_profiles[row + 1]; + return {lexical_cast((*profile)[col], m_doc->GetStyle().NumDecimals()).c_str(), + *wxConvCurrent}; + } + + void SetValue(int, int, const wxString &) override {} + + wxGridCellAttr *GetAttr(int row, int col, wxGridCellAttr::wxAttrKind) override + { + auto *attr = new wxGridCellAttr(); + attr->SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL)); + attr->SetAlignment(wxALIGN_RIGHT, wxALIGN_CENTRE); + attr->SetReadOnly(true); + + if (col > 0) { + attr->SetTextColour(m_doc->GetStyle().GetPlayerColor(Traits::ColumnPlayer(m_doc, col))); + attr->SetBackgroundColour(Traits::ColumnGroup(m_doc, col) % 2 == 0 + ? wxColour(250, 250, 250) + : wxColour(225, 225, 225)); + } + else { + attr->SetTextColour(*wxBLACK); + attr->SetBackgroundColour(wxColour(250, 250, 250)); + } + + return attr; + } + + //! + //! wxGrid::SetColLabelValue/SetCornerLabelValue on the grid just forward + //! to the table's own (default no-op) methods of the same name, so + //! headers must be supplied by overriding the getters here instead. + //! + wxString GetColLabelValue(int col) override + { + if (col == 0) { + return wxT("Lambda"); + } + return Traits::ColumnLabel(m_doc, col); + } + wxString GetCornerLabelValue() const override { return wxT("#"); } + + void AddProfile(const typename Traits::QREType &p_qre, bool p_forceShow) + { + wxGrid *view = GetView(); + + if (m_numCols == 0) { + m_numCols = static_cast(p_qre.size()) + 1; + if (view) { + wxGridTableMessage msg(this, wxGRIDTABLE_NOTIFY_COLS_APPENDED, m_numCols); + view->ProcessTableMessage(msg); + } + } + + m_lambdas.push_back(p_qre.GetLambda()); + m_profiles.push_back(std::make_shared(p_qre.GetProfile())); + + if (p_forceShow || static_cast(m_profiles.size()) - m_numRows > 20) { + const int newRows = static_cast(m_profiles.size()) - m_numRows; + m_numRows = static_cast(m_profiles.size()); + if (view) { + wxGridTableMessage msg(this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, newRows); + view->ProcessTableMessage(msg); + view->MakeCellVisible(m_numRows - 1, 0); + } + } + + // Lambda tends to get large, so this column usually needs resized + if (view) { + view->AutoSizeColumn(0); + } + } +}; + +//========================================================================= +// class LogitGrid +//========================================================================= + +template class LogitGrid final : public wxGrid { + LogitTable *m_gridTable; + + //! + //! @name Overriding wxGrid behavior to disable selection & the cursor + //! highlight rectangle, matching the read-only, non-interactive table + //! this replaces. + //! + //@{ + void OnRangeSelecting(wxGridRangeSelectEvent &p_event) { p_event.Veto(); } + void OnSelectCell(wxGridEvent &p_event) { p_event.Skip(); } + //@} + +public: + LogitGrid(wxWindow *p_parent, const std::shared_ptr &p_doc) + : wxGrid(p_parent, wxID_ANY), m_gridTable(new LogitTable(p_doc)) + { + SetTable(m_gridTable, true); + EnableEditing(false); + SetRowLabelSize(FromDIP(40)); + SetColLabelSize(FromDIP(25)); + + SetCellHighlightPenWidth(0); + SetCellHighlightROPenWidth(0); + + Bind(wxEVT_GRID_RANGE_SELECTING, &LogitGrid::OnRangeSelecting, this); + Bind(wxEVT_GRID_SELECT_CELL, &LogitGrid::OnSelectCell, this); + } + + void AddProfile(const typename Traits::QREType &p_qre, bool p_forceShow) + { + m_gridTable->AddProfile(p_qre, p_forceShow); + } +}; + +// +// Traces the logit equilibrium correspondence directly, in-process, on a +// worker thread, rather than shelling out to gambit-logit and parsing its +// output. Posts each point on the branch back to the dialog as it is found. +// +template class LogitThreadRunner final : public wxThread { + wxEvtHandler *m_parent; + Game m_game; + CancelToken m_cancel; + + void PostPoint(const typename Traits::QREType &p_qre) const + { + auto *event = new wxThreadEvent(wxEVT_LOGIT_POINT); + event->SetPayload(p_qre); + wxQueueEvent(m_parent, event); + } + + ExitCode Entry() override + { + int exitCode = 0; + try { + Traits::Solve( + m_game, + [this](const LogitEvent &p_event) { + PostPoint(std::get>(p_event).qre); + }, + m_cancel); + } + catch (const ComputationCanceledException &) { + // Not an error -- LogitDialog's m_stopRequested flag (already set before + // RequestCancel() was called) is what determines that the "finished" event below is + // reported as a stop rather than a failure. + } + catch (const std::exception &) { + exitCode = 1; + } + + auto *evt = new wxThreadEvent(wxEVT_LOGIT_FINISHED); + evt->SetInt(exitCode); + wxQueueEvent(m_parent, evt); + return static_cast(0); + } + +public: + LogitThreadRunner(wxEvtHandler *p_parent, Game p_game) + : wxThread(wxTHREAD_JOINABLE), m_parent(p_parent), m_game(std::move(p_game)) + { + } + + void RequestCancel() { m_cancel.RequestCancel(); } +}; + +//========================================================================= +// class LogitDialog +//========================================================================= + +template class LogitDialog final : public wxDialog { + std::shared_ptr m_doc; + std::unique_ptr> m_runner; + LogitGrid *m_list; + wxStaticText *m_statusText; + wxButton *m_stopButton, *m_okButton, *m_saveButton; + wxString m_output; + bool m_stopRequested{false}; + + void Start() + { + Traits::PrepareGame(m_doc); + + m_runner = std::make_unique>(this, m_doc->GetGame()); + if (m_runner->Run() != wxTHREAD_NO_ERROR) { + m_runner.reset(); + m_statusText->SetLabel(wxT("Failed to launch computation.")); + m_statusText->SetForegroundColour(*wxRED); + m_okButton->Enable(true); + } + } + + void OnRunnerPoint(wxThreadEvent &p_event) + { + const auto &qre = p_event.GetPayload(); + m_list->AddProfile(qre, false); + + const int decimals = m_doc->GetStyle().NumDecimals(); + wxString line(lexical_cast(qre.GetLambda(), decimals).c_str(), *wxConvCurrent); + for (size_t i = 1; i <= qre.size(); i++) { + line += + wxT(",") + wxString(lexical_cast(qre[i], decimals).c_str(), *wxConvCurrent); + } + m_output += line + wxT("\n"); + } + + void OnRunnerFinished(wxThreadEvent &p_event) + { + m_stopButton->Enable(false); + + if (m_runner) { + // Joinable thread: must be waited on before it can be safely destroyed. Entry() has + // already posted this event and is returning, so this should not block for any + // appreciable time. + m_runner->Wait(); + m_runner.reset(); + } + + if (m_stopRequested) { + m_statusText->SetLabel(wxT("The computation has been stopped.")); + m_statusText->SetForegroundColour(wxColour(196, 128, 0)); + } + else if (p_event.GetInt() == 0) { + m_statusText->SetLabel(wxT("The computation has completed.")); + m_statusText->SetForegroundColour(wxColour(0, 192, 0)); + } + else { + m_statusText->SetLabel(wxT("The computation ended abnormally.")); + m_statusText->SetForegroundColour(*wxRED); + } + + m_okButton->Enable(true); + m_saveButton->Enable(true); + } + + void OnStop(wxCommandEvent &) + { + m_stopRequested = true; + m_stopButton->Enable(false); + if (m_runner) { + m_runner->RequestCancel(); + } + } + + void OnClose(wxCloseEvent &p_event) + { + if (!m_runner || !m_stopButton->IsEnabled()) { + p_event.Skip(); + return; + } + + m_stopRequested = true; + m_stopButton->Enable(false); + m_runner->RequestCancel(); + + if (p_event.CanVeto()) { + p_event.Veto(); + } + else { + p_event.Skip(); + } + } + + void OnSave(wxCommandEvent &) + { + wxFileDialog dialog(this, _("Choose file"), wxT(""), wxT(""), + wxT("CSV files (*.csv)|*.csv|") wxT("All files (*.*)|*.*"), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (dialog.ShowModal() == wxID_OK) { + std::ofstream file(dialog.GetPath().mb_str()); + file << static_cast(m_output.mb_str()); + } + } + +public: + LogitDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) + : wxDialog(p_parent, wxID_ANY, wxT("Compute quantal response equilibria"), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_doc(p_doc), m_list(new LogitGrid(this, m_doc)) + { + const int S = FromDIP(5); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *startSizer = new wxBoxSizer(wxHORIZONTAL); + + m_statusText = + new wxStaticText(this, wxID_STATIC, wxT("The computation is currently in progress.")); + m_statusText->SetForegroundColour(*wxBLUE); + startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, S); + + m_stopButton = new wxBitmapButton(this, wxID_CANCEL, wxBitmap(stop_xpm)); + m_stopButton->SetToolTip(_("Stop the computation")); + startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, S); + m_stopButton->Bind(wxEVT_BUTTON, &LogitDialog::OnStop, this); + + sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, S); + + m_list->SetSizeHints(wxSize(FromDIP(600), FromDIP(400))); + sizer->Add(m_list, 1, wxALL | wxEXPAND, S); + + auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); + m_saveButton = new wxButton(this, wxID_SAVE, wxT("Save correspondence to .csv file")); + m_saveButton->Enable(false); + buttonSizer->Add(m_saveButton, 0, wxALL | wxALIGN_CENTER, S); + m_saveButton->Bind(wxEVT_BUTTON, &LogitDialog::OnSave, this); + + buttonSizer->AddStretchSpacer(); + buttonSizer->Add(CreateStdDialogButtonSizer(wxOK), 0, wxALL | wxALIGN_CENTER, S); + m_okButton = dynamic_cast(FindWindow(wxID_OK)); + m_okButton->Enable(false); + + sizer->Add(buttonSizer, 0, wxEXPAND | wxALL, S); + + SetSizer(sizer); + sizer->SetSizeHints(this); + SetSize(GetBestSize()); + SetMinSize(GetSize()); + CenterOnParent(); + + Bind(wxEVT_LOGIT_POINT, &LogitDialog::OnRunnerPoint, this); + Bind(wxEVT_LOGIT_FINISHED, &LogitDialog::OnRunnerFinished, this); + Bind(wxEVT_CLOSE_WINDOW, &LogitDialog::OnClose, this); + + Start(); + } + + ~LogitDialog() override = default; +}; + +using LogitBehavDialog = LogitDialog; +using LogitMixedDialog = LogitDialog; + +void LogitStrategic(wxWindow *p_parent, const std::shared_ptr &p_doc); + +} // namespace Gambit::GUI + +#endif // GAMBIT_GUI_DLLOGIT_H