Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
26894bd
Refactor concept of an analysis workspace
tturocy Jun 3, 2026
85add90
Implement separate workspace modification notification
tturocy Jun 3, 2026
eec032b
Access const state of profile lists
tturocy Jun 3, 2026
acf89a6
Access const state of dominance stack
tturocy Jun 3, 2026
37813b8
Standardise on "Do" to name document mutating operations
tturocy Jun 3, 2026
b3d7860
Access game directly rather than forwarding some properties
tturocy Jun 3, 2026
e9c176a
Some renaming in workspace
tturocy Jun 3, 2026
c667b88
Modernise change types
tturocy Jun 3, 2026
a55b000
Refer to loading workspace document
tturocy Jun 3, 2026
62f2821
Nips and tucks to GUI documentation on workspaces
tturocy Jun 3, 2026
507a1d7
workbook -> workspace
tturocy Jun 3, 2026
148d38c
save workspace not document
tturocy Jun 3, 2026
00308db
Have application exit close windows one by one let each one veto if n…
tturocy Jun 3, 2026
8627c9e
Merge branch 'master' into dev_workspace
tturocy Jun 3, 2026
97ec977
Merge branch 'master' into dev_workspace
tturocy Jun 5, 2026
27e62d9
Merge branch 'master' into dev_workspace
tturocy Jun 19, 2026
611956b
Merge fixups
tturocy Jun 19, 2026
dc213d0
Merge branch 'master' into dev_workspace
tturocy Jun 19, 2026
a969ad2
Merge branch 'master' into dev_workspace
tturocy Jun 29, 2026
db7026b
Keep welcome frame if file load fails.
tturocy Jun 29, 2026
13b8ed9
Simplify file saving to use only the native format.
tturocy Jun 29, 2026
cf979f0
Switch to preferring the native format
tturocy Jun 29, 2026
fccbd23
Updated ChangeLog
tturocy Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
- In `pygambit`, indexing game object collections by integer position has been removed. (#942)
- Validity of game object labels is enforced (printable ASCII and spaces only, no leading/trailing or
double spaces); invalid labels raise `ValueError` in `pygambit`. (#944)
- Refined and clarified the graphical interface's handling (and persisting) of "workspaces", and
improved warning messages on closing windows for games or workspaces with unsaved changes.

### Removed
- Built-in plotting of logit QRE for strategic games has been removed in the GUI (#809)
Expand Down
2 changes: 1 addition & 1 deletion doc/gui.efg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ To select the font used to draw the labels in the tree, select
:menuselection:`Format --> Font`.
The standard font selection dialog for the operating
system is displayed, showing the fonts available on the system. Since
available fonts vary across systems, when opening a workbook on a
available fonts vary across systems, when opening a workspace on a
system different from the system on which it was saved, Gambit tries
to match the font style as closely as possible when the original font
is not available.
Expand Down
26 changes: 5 additions & 21 deletions doc/gui.general.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,30 +80,14 @@ versions dating back to release 0.94 in 1995. (Users interested in the
details of these file formats can consult :ref:`file-formats`
for more information.)

Beginning with release 2005.12.xx, the graphical interface now reads
and writes a new file format, which is referred to as a"Gambit
workbook." This extended file format stores not only the
The graphical interface reads and writes an extended format, referred to
as a "Gambit workspace".
This stores not only the
representation of the game, but also additional information, including
parameters for laying out the game tree, the colors assigned to
players, any equilibria or other analysis done on the game, and so
forth. So, for example, the workbook file can be used to store the
forth. So, for example, the workspace file can be used to store the
analysis of a game and then return to it. These files by convention
end in the extension .gbt.

The graphical interface will read files in all three formats: .gbt,
.efg, and .nfg. The "Save" and "Save as" commands, however, always
save in the Gambit workbook (.gbt) format. To save the game itself as
an extensive (.efg) or strategic (.nfg) game, use the items on the
"Export" submenu of the "File" menu. This is useful in interfacing
with older versions of Gambit, with other tools which read and write
those formats, and in using the underlying Gambit analysis command-
line tools directly, as those programs accept .efg or .nfg game files.
Users primarily interested in using Gambit solely via the graphical
interface are encouraged to use the workbook (.gbt) format.



As it is a new format, the Gambit workbook format is still under
development and may change in details. It is intended that newer
versions of the graphical interface will still be able to read
workbook files written in older formats.
.efg, and .nfg.
8 changes: 4 additions & 4 deletions src/gui/analysis.cc
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ template <class T> void AnalysisProfileList<T>::AddOutput(const wxString &p_outp
auto profile =
std::make_shared<MixedStrategyProfile<T>>(OutputToMixedProfile<T>(m_doc, p_output));
m_mixedProfiles.push_back(profile);
if (m_doc->IsTree()) {
if (m_doc->GetGame()->IsTree()) {
m_behavProfiles.push_back(std::make_shared<MixedBehaviorProfile<T>>(*profile));
}
m_current = m_mixedProfiles.size();
Expand All @@ -120,7 +120,7 @@ template <class T> void AnalysisProfileList<T>::BuildNfg()

template <class T> int AnalysisProfileList<T>::NumProfiles() const
{
return (m_doc->IsTree()) ? m_behavProfiles.size() : m_mixedProfiles.size();
return (m_doc->GetGame()->IsTree()) ? m_behavProfiles.size() : m_mixedProfiles.size();
}

template <class T> void AnalysisProfileList<T>::Clear()
Expand Down Expand Up @@ -202,7 +202,7 @@ template <class T> std::string AnalysisProfileList<T>::GetPayoff(int pl, int p_i
const int index = (p_index == -1) ? m_current : p_index;

try {
if (m_doc->IsTree()) {
if (m_doc->GetGame()->IsTree()) {
return lexical_cast<std::string>(
m_behavProfiles[index]->GetPayoff(m_doc->GetGame()->GetPlayer(pl)),
m_doc->GetStyle().NumDecimals());
Expand Down Expand Up @@ -417,7 +417,7 @@ template <class T> void AnalysisProfileList<T>::Save(std::ostream &p_file) const
p_file << static_cast<const char *>(m_description.mb_str()) << "\n";
p_file << "</description>\n";

if (m_doc->IsTree()) {
if (m_doc->GetGame()->IsTree()) {
for (int j = 1; j <= NumProfiles(); j++) {
const MixedBehaviorProfile<T> &behav = *m_behavProfiles[j];
p_file << "<profile type=\"behav\">\n";
Expand Down
8 changes: 1 addition & 7 deletions src/gui/app.cc
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ AppLoadResult Application::LoadFile(const wxString &p_filename, wxWindow *p_pare
}

auto *doc = new GameDocument(NewTree());
if (doc->LoadDocument(p_filename)) {
if (doc->LoadWorkspace(p_filename)) {
doc->SetFilename(p_filename);
m_fileHistory.AddFileToHistory(p_filename);
m_fileHistory.Save(*wxConfigBase::Get());
Expand Down Expand Up @@ -167,12 +167,6 @@ void Application::SetCurrentDir(const wxString &p_dir)
wxConfigBase::Get()->Write(_T("/General/CurrentDirectory"), p_dir);
}

bool Application::AreDocumentsModified() const
{
return std::any_of(m_documents.begin(), m_documents.end(),
std::mem_fn(&GameDocument::IsModified));
}

} // namespace Gambit::GUI

IMPLEMENT_APP(Gambit::GUI::Application)
1 change: 0 additions & 1 deletion src/gui/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ class Application final : public wxApp {
{
m_documents.erase(std::find(m_documents.begin(), m_documents.end(), p_doc));
}
bool AreDocumentsModified() const;
//@}
};

Expand Down
2 changes: 1 addition & 1 deletion src/gui/dlgameprop.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ GamePropertiesDialog::GamePropertiesDialog(wxWindow *p_parent, GameDocument *p_d
wxALL, 5);
}

if (m_doc->IsTree()) {
if (game->IsTree()) {
if (game->IsPerfectRecall()) {
boxSizer->Add(new wxStaticText(this, wxID_STATIC, _("This is a game of perfect recall")), 0,
wxALL, 5);
Expand Down
8 changes: 4 additions & 4 deletions src/gui/dlinsertmove.cc
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ void InsertMoveDialog::OnPlayer(wxCommandEvent &)
if (playerNumber == 0) {
player = m_doc->GetGame()->GetChance();
}
else if (playerNumber <= static_cast<int>(m_doc->NumPlayers())) {
else if (playerNumber <= static_cast<int>(m_doc->GetGame()->NumPlayers())) {
player = m_doc->GetGame()->GetPlayer(playerNumber);
}

Expand Down Expand Up @@ -187,17 +187,17 @@ GamePlayer InsertMoveDialog::GetPlayer() const
if (playerNumber == 0) {
return m_doc->GetGame()->GetChance();
}
if (playerNumber <= static_cast<int>(m_doc->NumPlayers())) {
if (playerNumber <= static_cast<int>(m_doc->GetGame()->NumPlayers())) {
return m_doc->GetGame()->GetPlayer(playerNumber);
}
const GamePlayer player = m_doc->GetGame()->NewPlayer();
player->SetLabel("Player " + lexical_cast<std::string>(m_doc->NumPlayers()));
player->SetLabel("Player " + lexical_cast<std::string>(m_doc->GetGame()->NumPlayers()));
return player;
}

GameInfoset InsertMoveDialog::GetInfoset() const
{
if (m_playerItem->GetSelection() <= static_cast<int>(m_doc->NumPlayers())) {
if (m_playerItem->GetSelection() <= static_cast<int>(m_doc->GetGame()->NumPlayers())) {
const GamePlayer player = GetPlayer();
const int infosetNumber = m_infosetItem->GetSelection();

Expand Down
16 changes: 8 additions & 8 deletions src/gui/dlnash.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, GameDocument *p_doc)
wxCommandEventHandler(NashChoiceDialog::OnCount));
topSizer->Add(m_countChoice, 0, wxALL | wxEXPAND, 5);

if (p_doc->NumPlayers() == 2 && m_doc->IsConstSum()) {
if (p_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) {
wxString methodChoices[] = {s_recommended, s_lp, s_simpdiv, s_logit, s_enumpoly};
m_methodChoice =
new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, methodChoices);
Expand All @@ -79,7 +79,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, GameDocument *p_doc)
m_methodChoice->SetSelection(0);
topSizer->Add(m_methodChoice, 0, wxALL | wxEXPAND, 5);

if (m_doc->IsTree()) {
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);
Expand Down Expand Up @@ -114,14 +114,14 @@ void NashChoiceDialog::OnCount(wxCommandEvent &p_event)
m_methodChoice->Append(s_recommended);

if (p_event.GetSelection() == 0) {
if (m_doc->NumPlayers() == 2 && m_doc->IsConstSum()) {
if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) {
m_methodChoice->Append(s_lp);
}
m_methodChoice->Append(s_simpdiv);
m_methodChoice->Append(s_logit);
}
else if (p_event.GetSelection() == 1) {
if (m_doc->NumPlayers() == 2) {
if (m_doc->GetGame()->NumPlayers() == 2) {
m_methodChoice->Append(s_lcp);
}
m_methodChoice->Append(s_enumpure);
Expand All @@ -131,7 +131,7 @@ void NashChoiceDialog::OnCount(wxCommandEvent &p_event)
m_methodChoice->Append(s_enumpoly);
}
else {
if (m_doc->NumPlayers() == 2) {
if (m_doc->GetGame()->NumPlayers() == 2) {
m_methodChoice->Append(s_enummixed);
}
}
Expand Down Expand Up @@ -195,7 +195,7 @@ std::shared_ptr<AnalysisOutput> NashChoiceDialog::GetCommand() const

if (method == s_recommended) {
if (m_countChoice->GetSelection() == 0) {
if (m_doc->NumPlayers() == 2 && m_doc->IsConstSum()) {
if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) {
cmd = std::make_shared<AnalysisProfileList<Rational>>(m_doc, useEfg);
cmd->SetCommand(prefix + wxT("lp") + options);
cmd->SetDescription(wxT("One equilibrium by solving a linear program ") + game);
Expand All @@ -207,7 +207,7 @@ std::shared_ptr<AnalysisOutput> NashChoiceDialog::GetCommand() const
}
}
else if (m_countChoice->GetSelection() == 1) {
if (m_doc->NumPlayers() == 2) {
if (m_doc->GetGame()->NumPlayers() == 2) {
cmd = std::make_shared<AnalysisProfileList<Rational>>(m_doc, useEfg);
cmd->SetCommand(prefix + wxT("lcp") + options);
cmd->SetDescription(wxT("Some equilibria by solving a linear complementarity program ") +
Expand All @@ -220,7 +220,7 @@ std::shared_ptr<AnalysisOutput> NashChoiceDialog::GetCommand() const
}
}
else {
if (m_doc->NumPlayers() == 2) {
if (m_doc->GetGame()->NumPlayers() == 2) {
cmd = std::make_shared<AnalysisProfileList<Rational>>(m_doc, false);
cmd->SetCommand(prefix + wxT("enummixed"));
cmd->SetDescription(
Expand Down
2 changes: 1 addition & 1 deletion src/gui/dlnashmon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ void NashMonitorDialog::Start(std::shared_ptr<AnalysisOutput> p_command)
m_doc->BuildNfg();
}

m_doc->AddProfileList(p_command);
m_doc->DoAddEquilibriumOutput(p_command);

m_process = new wxProcess(this, GBT_ID_PROCESS);
m_process->Redirect();
Expand Down
4 changes: 2 additions & 2 deletions src/gui/efgdisplay.cc
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ void OutcomeEditorPopup::BuildControls()

const Game game = m_doc->GetGame();

for (size_t player = 1; player <= m_doc->NumPlayers(); ++player) {
for (size_t player = 1; player <= m_doc->GetGame()->NumPlayers(); ++player) {
const GamePlayer gamePlayer = game->GetPlayer(player);

payoffSizer->Add(new wxStaticText(m_contentPanel, wxID_ANY,
Expand Down Expand Up @@ -1144,7 +1144,7 @@ void EfgDisplay::OnLeftDoubleClick(wxMouseEvent &p_event)
if (node->GetOutcome()) {
auto entry = m_layout.GetNodeEntry(node);

for (size_t player = 1; player <= m_doc->NumPlayers(); ++player) {
for (size_t player = 1; player <= m_doc->GetGame()->NumPlayers(); ++player) {
if (entry->GetPayoffExtent(player).Contains(x, y)) {
initialPlayer = static_cast<int>(player);
break;
Expand Down
30 changes: 20 additions & 10 deletions src/gui/efglayout.cc
Original file line number Diff line number Diff line change
Expand Up @@ -481,12 +481,15 @@ wxString TreeLayout::CreateNodeLabel(const std::shared_ptr<NodeEntry> &p_entry,
return _T("");
}
case GBT_NODE_LABEL_REALIZPROB:
return {m_doc->GetProfiles().GetRealizProb(n).c_str(), *wxConvCurrent};
return {m_doc->GetWorkspace().GetProfiles().GetRealizProb(n).c_str(), *wxConvCurrent};
case GBT_NODE_LABEL_BELIEFPROB:
return {m_doc->GetProfiles().GetBeliefProb(n).c_str(), *wxConvCurrent};
return {m_doc->GetWorkspace().GetProfiles().GetBeliefProb(n).c_str(), *wxConvCurrent};
case GBT_NODE_LABEL_VALUE:
if (n->GetInfoset() && n->GetPlayer()->GetNumber() > 0) {
return {m_doc->GetProfiles().GetNodeValue(n, n->GetPlayer()->GetNumber()).c_str(),
return {m_doc->GetWorkspace()
.GetProfiles()
.GetNodeValue(n, n->GetPlayer()->GetNumber())
.c_str(),
*wxConvCurrent};
}
else {
Expand Down Expand Up @@ -521,19 +524,25 @@ wxString TreeLayout::CreateBranchLabel(const std::shared_ptr<NodeEntry> &p_entry
.c_str(),
*wxConvCurrent};
}
else if (m_doc->NumProfileLists() == 0) {
else if (m_doc->GetWorkspace().NumProfileLists() == 0) {
return wxT("");
}
else {
return {m_doc->GetProfiles().GetActionProb(parent, p_entry->GetChildNumber()).c_str(),
return {m_doc->GetWorkspace()
.GetProfiles()
.GetActionProb(parent, p_entry->GetChildNumber())
.c_str(),
*wxConvCurrent};
}
case GBT_BRANCH_LABEL_VALUE:
if (m_doc->NumProfileLists() == 0) {
if (m_doc->GetWorkspace().NumProfileLists() == 0) {
return wxT("");
}
else {
return {m_doc->GetProfiles().GetActionValue(parent, p_entry->GetChildNumber()).c_str(),
return {m_doc->GetWorkspace()
.GetProfiles()
.GetActionValue(parent, p_entry->GetChildNumber())
.c_str(),
*wxConvCurrent};
}
default:
Expand Down Expand Up @@ -712,11 +721,12 @@ void TreeLayout::GenerateLabels() const
parent->GetInfoset()->GetAction(entry->GetChildNumber()))));
}
else {
const int profile = m_doc->GetCurrentProfile();
const int profile = m_doc->GetWorkspace().GetCurrentProfile();
if (profile > 0) {
try {
entry->SetActionProb((double)lexical_cast<Rational>(
m_doc->GetProfiles().GetActionProb(parent, entry->GetChildNumber())));
entry->SetActionProb(
(double)lexical_cast<Rational>(m_doc->GetWorkspace().GetProfiles().GetActionProb(
parent, entry->GetChildNumber())));
}
catch (ValueException &) {
// This occurs when the probability is undefined
Expand Down
22 changes: 11 additions & 11 deletions src/gui/efgpanel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ gbtTreePlayerPanel::gbtTreePlayerPanel(wxWindow *p_parent, GameDocument *p_doc,

void gbtTreePlayerPanel::OnUpdate()
{
if (!m_doc->IsTree()) {
if (!m_doc->GetGame()->IsTree()) {
return;
}

Expand All @@ -191,36 +191,36 @@ void gbtTreePlayerPanel::OnUpdate()
wxString(m_doc->GetGame()->GetPlayer(m_player)->GetLabel().c_str(), *wxConvCurrent));

m_payoff->SetForegroundColour(color);
if (m_doc->GetCurrentProfile() > 0) {
const std::string pay = m_doc->GetProfiles().GetPayoff(m_player);
if (m_doc->GetWorkspace().GetCurrentProfile() > 0) {
const std::string pay = m_doc->GetWorkspace().GetProfiles().GetPayoff(m_player);
m_payoff->SetLabel(wxT("Payoff: ") + wxString(pay.c_str(), *wxConvCurrent));
GetSizer()->Show(m_payoff, true);

if (const GameNode node = m_doc->GetSelectNode()) {
m_nodeValue->SetForegroundColour(color);
std::string value = m_doc->GetProfiles().GetNodeValue(node, m_player);
std::string value = m_doc->GetWorkspace().GetProfiles().GetNodeValue(node, m_player);
m_nodeValue->SetLabel(wxT("Node value: ") + wxString(value.c_str(), *wxConvCurrent));
GetSizer()->Show(m_nodeValue, true);

if (node->GetInfoset() && node->GetPlayer()->GetNumber() == m_player) {
m_nodeProb->SetForegroundColour(color);
value = m_doc->GetProfiles().GetRealizProb(node);
value = m_doc->GetWorkspace().GetProfiles().GetRealizProb(node);
m_nodeProb->SetLabel(wxT("Node reached: ") + wxString(value.c_str(), *wxConvCurrent));
GetSizer()->Show(m_nodeProb, true);

m_infosetValue->SetForegroundColour(color);
value = m_doc->GetProfiles().GetInfosetValue(node);
value = m_doc->GetWorkspace().GetProfiles().GetInfosetValue(node);
m_infosetValue->SetLabel(wxT("Infoset value: ") + wxString(value.c_str(), *wxConvCurrent));
GetSizer()->Show(m_infosetValue, true);

m_infosetProb->SetForegroundColour(color);
value = m_doc->GetProfiles().GetInfosetProb(node);
value = m_doc->GetWorkspace().GetProfiles().GetInfosetProb(node);
m_infosetProb->SetLabel(wxT("Infoset reached: ") +
wxString(value.c_str(), *wxConvCurrent));
GetSizer()->Show(m_infosetProb, true);

m_belief->SetForegroundColour(color);
value = m_doc->GetProfiles().GetBeliefProb(node);
value = m_doc->GetWorkspace().GetProfiles().GetBeliefProb(node);
m_belief->SetLabel(wxT("Belief: ") + wxString(value.c_str(), *wxConvCurrent));
GetSizer()->Show(m_belief, true);
}
Expand Down Expand Up @@ -443,7 +443,7 @@ gbtTreePlayerToolbar::gbtTreePlayerToolbar(wxWindow *p_parent, GameDocument *p_d

topSizer->Add(m_chancePanel, 0, wxALL | wxEXPAND, 5);

for (size_t pl = 1; pl <= m_doc->NumPlayers(); pl++) {
for (size_t pl = 1; pl <= m_doc->GetGame()->NumPlayers(); pl++) {
m_playerPanels.push_back(new gbtTreePlayerPanel(this, m_doc, pl));
topSizer->Add(m_playerPanels[pl], 0, wxALL | wxEXPAND, 5);
}
Expand All @@ -454,13 +454,13 @@ gbtTreePlayerToolbar::gbtTreePlayerToolbar(wxWindow *p_parent, GameDocument *p_d

void gbtTreePlayerToolbar::OnUpdate()
{
while (m_playerPanels.size() < m_doc->NumPlayers()) {
while (m_playerPanels.size() < m_doc->GetGame()->NumPlayers()) {
auto *panel = new gbtTreePlayerPanel(this, m_doc, m_playerPanels.size() + 1);
m_playerPanels.push_back(panel);
GetSizer()->Add(panel, 0, wxALL | wxEXPAND, 5);
}

while (m_playerPanels.size() > m_doc->NumPlayers()) {
while (m_playerPanels.size() > m_doc->GetGame()->NumPlayers()) {
gbtTreePlayerPanel *panel = m_playerPanels.back();
GetSizer()->Detach(panel);
panel->Destroy();
Expand Down
Loading
Loading