diff --git a/.all-contributorsrc b/.all-contributorsrc index 4c6d50849..ca564aa9b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -165,7 +165,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/83069628?v=4", "profile": "https://github.com/ameliekleber", "contributions": [ - "test" + "test", + "code" ] } ], diff --git a/ChangeLog b/ChangeLog index 67e40760a..98715ee9c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -38,6 +38,7 @@ - Added `Game.get_strategies(player)`, `Game.get_sequences(player)`, `Game.get_min_payoff(player)`, and `Game.get_max_payoff(player)`, replacing `Player.strategies`/`.sequences`/`.min_payoff`/ `.max_payoff` now that `Player` has been removed. +- In the GUI, computed strategy and behaviour profiles can now be sorted by probability (#1077) ### Changed - `Infoset` is now a lazy, node-anchored view (like `Node.player`/`Node.outcome`), constructed diff --git a/Makefile.am b/Makefile.am index 0e0eb8f55..7a059f072 100644 --- a/Makefile.am +++ b/Makefile.am @@ -431,6 +431,10 @@ gambit_SOURCES = \ src/gui/nfgprofile.h \ src/gui/nfgtable.cc \ src/gui/nfgtable.h \ + src/gui/profilelabels.cc \ + src/gui/profilelabels.h \ + src/gui/profilesort.cc \ + src/gui/profilesort.h \ src/gui/renratio.cc \ src/gui/renratio.h \ src/gui/style.cc \ diff --git a/README.md b/README.md index a8e43b9f7..29c8b6415 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ pip install pygambit S Kunath
S Kunath

💻 Alessandro Andrioni
Alessandro Andrioni

💻 Ewout ter Hoeven
Ewout ter Hoeven

🚇 💻 - ameliekleber
ameliekleber

⚠️ + ameliekleber
ameliekleber

⚠️ 💻 diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index ce6906c42..2df8238e4 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -131,6 +131,23 @@ since only one computation has been run in this example, it shows brief description of the method used to compute the equilibria is listed across the top of the profiles panel. +By default the equilibria are listed in the order in which they were +computed. Clicking on the label of a column in the profiles listing +sorts the equilibria by the probability with which that action or +strategy is played; clicking on the same column label again reverses +the order. The label of the column sorted on is highlighted and +carries an arrow, :guilabel:`▲` or :guilabel:`▼`, showing the +direction of the sort. Equilibria which agree on that +column are ordered by +comparing their profiles entry-by-entry from the leftmost column +onwards, so sorting on the first column lists the equilibria in +lexicographic order by profile. Clicking on the :guilabel:`#` label in +the top left corner of the listing restores the order in which the +equilibria were computed. Sorting only changes the order in which the +equilibria are listed: the number shown at the left of each row always +identifies the equilibrium itself, so it does not change when the list +is re-sorted. + Some methods for computing equilibria construct good numerical approximations to equilibrium points rather than exact values; for these methods, the computed equilibria are stored in floating-point diff --git a/src/gui/analysis.cc b/src/gui/analysis.cc index 08b2b35dc..fe0f49627 100644 --- a/src/gui/analysis.cc +++ b/src/gui/analysis.cc @@ -346,6 +346,73 @@ std::string AnalysisProfileList::GetStrategyValue(int p_strategy, int p_index } } +namespace { + +/// Order two entries, either of which may be undefined; undefined entries +/// order after defined ones. Only operator< is used, so entries are ordered +/// exactly in whatever type the profiles are stored in. +template +int CompareEntries(const std::optional &p_left, const std::optional &p_right) +{ + if (!p_left.has_value()) { + return p_right.has_value() ? 1 : 0; + } + if (!p_right.has_value()) { + return -1; + } + if (p_left.value() < p_right.value()) { + return -1; + } + if (p_right.value() < p_left.value()) { + return 1; + } + return 0; +} + +} // end anonymous namespace + +template +std::optional AnalysisProfileList::GetActionProbEntry(int p_action, int p_index) const +{ + try { + const MixedBehaviorProfile &profile = *m_behavProfiles[p_index]; + + if (!profile.IsDefinedAt(m_doc->GetAction(p_action)->GetInfoset())) { + return {}; + } + + return profile[p_action]; + } + catch (std::out_of_range &) { + return {}; + } +} + +template +std::optional AnalysisProfileList::GetStrategyProbEntry(int p_strategy, int p_index) const +{ + try { + return (*m_mixedProfiles[p_index])[p_strategy]; + } + catch (std::out_of_range &) { + return {}; + } +} + +template +int AnalysisProfileList::CompareActionProb(int p_action, int p_left, int p_right) const +{ + return CompareEntries(GetActionProbEntry(p_action, p_left), + GetActionProbEntry(p_action, p_right)); +} + +template +int AnalysisProfileList::CompareStrategyProb(int p_strategy, int p_left, int p_right) const +{ + return CompareEntries(GetStrategyProbEntry(p_strategy, p_left), + GetStrategyProbEntry(p_strategy, p_right)); +} + template LegacyWorkspaceFile::Analysis AnalysisProfileList::Save() const { LegacyWorkspaceFile::Analysis result; diff --git a/src/gui/analysis.h b/src/gui/analysis.h index c7e9d72f7..1114f42cd 100644 --- a/src/gui/analysis.h +++ b/src/gui/analysis.h @@ -115,6 +115,16 @@ class AnalysisOutput { virtual std::string GetStrategyProb(int p_strategy, int p_index = -1) const = 0; virtual std::string GetStrategyValue(int p_strategy, int p_index = -1) const = 0; + /// Compare the probability of an action in two profiles, for ordering + /// them; negative, zero, or positive as the probability in p_left is less + /// than, equal to, or greater than that in p_right. Probabilities are + /// compared in the type the profiles are stored in, so that exact + /// representations are ordered exactly. A profile which does not define + /// behavior at the action's information set orders after those which do. + virtual int CompareActionProb(int p_action, int p_left, int p_right) const = 0; + /// Compare the probability of a strategy in two profiles, as above + virtual int CompareStrategyProb(int p_strategy, int p_left, int p_right) const = 0; + /// Map all behavior profiles to corresponding mixed profiles virtual void BuildNfg() = 0; @@ -173,6 +183,8 @@ template class AnalysisProfileList final : public AnalysisOutput { std::string GetActionProb(int p_action, int p_index = -1) const override; std::string GetStrategyProb(int p_strategy, int p_index = -1) const override; std::string GetStrategyValue(int p_strategy, int p_index = -1) const override; + int CompareActionProb(int p_action, int p_left, int p_right) const override; + int CompareStrategyProb(int p_strategy, int p_left, int p_right) const override; /// Get the index of the currently selected profile int GetCurrent() const override { return m_current; } @@ -202,6 +214,13 @@ template class AnalysisProfileList final : public AnalysisOutput { void Load(const LegacyWorkspaceFile::Analysis &p_analysis); LegacyWorkspaceFile::Analysis Save() const override; //@} + +private: + /// The probability of an action in a profile, empty if the profile does + /// not define behavior at the action's information set + std::optional GetActionProbEntry(int p_action, int p_index) const; + /// The probability of a strategy in a profile + std::optional GetStrategyProbEntry(int p_strategy, int p_index) const; }; } // namespace Gambit::GUI diff --git a/src/gui/efgprofile.cc b/src/gui/efgprofile.cc index 43336acfe..934510933 100644 --- a/src/gui/efgprofile.cc +++ b/src/gui/efgprofile.cc @@ -38,6 +38,10 @@ MixedBehaviorProfileList::MixedBehaviorProfileList(wxWindow *p_parent, { CreateGrid(0, 0); + // The table takes ownership of the provider + m_labels = new ProfileLabelProvider; + GetTable()->SetAttrProvider(m_labels); + SetRowLabelSize(40); SetColLabelSize(25); SetCornerLabelValue(wxT("#")); @@ -50,6 +54,24 @@ MixedBehaviorProfileList::MixedBehaviorProfileList(wxWindow *p_parent, SetCellHighlightPenWidth(0); SetCellHighlightROPenWidth(0); +#if wxCHECK_VERSION(3, 3, 0) + // Suppress wxGrid's own label highlighting, which follows the grid cursor. + // The cursor is meaningless here, and cannot be moved out of the way, as + // wxGrid puts it back on the first cell whenever the grid is repainted; + // the labels worth highlighting are chosen in OnUpdate() instead. + // Versions before 3.3 do not highlight labels, so there is nothing to do. + DisableOverlaySelection(); +#endif + + // The column and corner labels sort the list, so show them as clickable + GetGridColLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); + GetGridCornerLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); + + GetGridColLabelWindow()->SetToolTip(_("Click an action to sort the profiles by its probability; " + "click it again to reverse the order")); + GetGridCornerLabelWindow()->SetToolTip( + _("Click to list the profiles in the order in which they were computed")); + Bind(wxEVT_GRID_LABEL_LEFT_CLICK, &MixedBehaviorProfileList::OnLabelClick, this); Bind(wxEVT_GRID_CELL_LEFT_CLICK, &MixedBehaviorProfileList::OnCellClick, this); Bind(wxEVT_GRID_SELECT_CELL, &MixedBehaviorProfileList::OnSelectCell, this); @@ -59,8 +81,20 @@ MixedBehaviorProfileList::~MixedBehaviorProfileList() = default; void MixedBehaviorProfileList::OnLabelClick(wxGridEvent &p_event) { - if (p_event.GetCol() == -1) { - m_doc->DoSelectProfile(p_event.GetRow() + 1); + if (p_event.GetCol() == -1 && p_event.GetRow() == -1) { + // The corner label: restore the order in which the profiles were computed + p_event.Veto(); + m_sortOrder.Reset(); + OnUpdate(); + } + else if (p_event.GetCol() == -1) { + m_doc->DoSelectProfile(m_sortOrder.GetProfile(p_event.GetRow())); + } + else if (p_event.GetRow() == -1) { + // An action: sort the profiles on the probability it is played with + p_event.Veto(); + m_sortOrder.ToggleColumn(p_event.GetCol() + 1); + OnUpdate(); } ClearSelection(); @@ -68,7 +102,7 @@ void MixedBehaviorProfileList::OnLabelClick(wxGridEvent &p_event) void MixedBehaviorProfileList::OnCellClick(wxGridEvent &p_event) { - m_doc->DoSelectProfile(p_event.GetRow() + 1); + m_doc->DoSelectProfile(m_sortOrder.GetProfile(p_event.GetRow())); ClearSelection(); } @@ -103,9 +137,11 @@ void MixedBehaviorProfileList::ResizeGrid(int p_rows, int p_cols) void MixedBehaviorProfileList::UpdateLabels() { + // Row labels identify the profile itself, so that a profile keeps its + // number however the list happens to be sorted. for (int row = 0; row < GetNumberRows(); ++row) { wxString label; - label << (row + 1); + label << m_sortOrder.GetProfile(row); SetRowLabelValue(row, label); } @@ -113,7 +149,8 @@ void MixedBehaviorProfileList::UpdateLabels() const GameAction action = m_doc->GetAction(col + 1); wxString label; - label << action->GetInfoset()->GetNumber() << ": " << action->GetLabel(); + label << action->GetInfoset()->GetNumber() << ": " << action->GetLabel() + << wxString::FromUTF8(m_sortOrder.GetColumnMarker(col + 1)); SetColLabelValue(col, label); } } @@ -126,14 +163,16 @@ void MixedBehaviorProfileList::UpdateCells() const wxFont boldFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD); for (int row = 0; row < GetNumberRows(); ++row) { + const int profile = m_sortOrder.GetProfile(row); + for (int col = 0; col < GetNumberCols(); ++col) { SetCellValue( row, col, - wxString(m_doc->GetWorkspace().GetProfiles().GetActionProb(col + 1, row + 1).c_str(), + wxString(m_doc->GetWorkspace().GetProfiles().GetActionProb(col + 1, profile).c_str(), *wxConvCurrent)); wxGridCellAttr *attr = new wxGridCellAttr; - attr->SetFont(row + 1 == currentProfile ? boldFont : normalFont); + attr->SetFont(profile == currentProfile ? boldFont : normalFont); attr->SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER); attr->SetReadOnly(true); @@ -158,6 +197,13 @@ void MixedBehaviorProfileList::UpdateCells() } } +void MixedBehaviorProfileList::UpdateHighlights() +{ + // GetColumn() numbers columns from one, and is zero when unsorted + m_labels->SetSortedColumn(m_sortOrder.GetColumn() - 1); + m_labels->SetSelectedRow(m_sortOrder.GetRow(m_doc->GetWorkspace().GetCurrentProfile())); +} + void MixedBehaviorProfileList::OnUpdate() { if (!m_doc->GetGame() || m_doc->GetWorkspace().NumProfileLists() == 0) { @@ -176,6 +222,11 @@ void MixedBehaviorProfileList::OnUpdate() BeginBatch(); ResizeGrid(profiles.NumProfiles(), profileLength); + m_sortOrder.Rebuild(profiles.NumProfiles(), profileLength, + [&](int p_col, int p_left, int p_right) { + return profiles.CompareActionProb(p_col, p_left, p_right); + }); + UpdateHighlights(); UpdateLabels(); UpdateCells(); diff --git a/src/gui/efgprofile.h b/src/gui/efgprofile.h index f21925bc9..7713deed7 100644 --- a/src/gui/efgprofile.h +++ b/src/gui/efgprofile.h @@ -26,9 +26,17 @@ #include #include "gamedoc.h" +#include "profilelabels.h" +#include "profilesort.h" namespace Gambit::GUI { class MixedBehaviorProfileList final : public wxGrid, public GameView { + /// The order in which the profiles are listed + ProfileSortOrder m_sortOrder; + /// Highlights the sorted column and the selected profile's row; owned by + /// the grid's table, which deletes it + ProfileLabelProvider *m_labels{nullptr}; + // Event handlers void OnLabelClick(wxGridEvent &); void OnCellClick(wxGridEvent &); @@ -36,6 +44,7 @@ class MixedBehaviorProfileList final : public wxGrid, public GameView { void ResizeGrid(int p_rows, int p_cols); void UpdateLabels(); + void UpdateHighlights(); void UpdateCells(); // Overriding GameView members diff --git a/src/gui/nfgprofile.cc b/src/gui/nfgprofile.cc index 855b8990f..e43d599ce 100644 --- a/src/gui/nfgprofile.cc +++ b/src/gui/nfgprofile.cc @@ -38,6 +38,10 @@ MixedStrategyProfileList::MixedStrategyProfileList(wxWindow *p_parent, { CreateGrid(0, 0); + // The table takes ownership of the provider + m_labels = new ProfileLabelProvider; + GetTable()->SetAttrProvider(m_labels); + SetRowLabelSize(40); SetColLabelSize(25); SetCornerLabelValue(wxT("#")); @@ -50,6 +54,25 @@ MixedStrategyProfileList::MixedStrategyProfileList(wxWindow *p_parent, SetCellHighlightPenWidth(0); SetCellHighlightROPenWidth(0); +#if wxCHECK_VERSION(3, 3, 0) + // Suppress wxGrid's own label highlighting, which follows the grid cursor. + // The cursor is meaningless here, and cannot be moved out of the way, as + // wxGrid puts it back on the first cell whenever the grid is repainted; + // the labels worth highlighting are chosen in OnUpdate() instead. + // Versions before 3.3 do not highlight labels, so there is nothing to do. + DisableOverlaySelection(); +#endif + + // The column and corner labels sort the list, so show them as clickable + GetGridColLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); + GetGridCornerLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); + + GetGridColLabelWindow()->SetToolTip( + _("Click a strategy to sort the profiles by its probability; " + "click it again to reverse the order")); + GetGridCornerLabelWindow()->SetToolTip( + _("Click to list the profiles in the order in which they were computed")); + Bind(wxEVT_GRID_LABEL_LEFT_CLICK, &MixedStrategyProfileList::OnLabelClick, this); Bind(wxEVT_GRID_CELL_LEFT_CLICK, &MixedStrategyProfileList::OnCellClick, this); Bind(wxEVT_GRID_SELECT_CELL, &MixedStrategyProfileList::OnSelectCell, this); @@ -59,8 +82,20 @@ MixedStrategyProfileList::~MixedStrategyProfileList() = default; void MixedStrategyProfileList::OnLabelClick(wxGridEvent &p_event) { - if (p_event.GetCol() == -1) { - m_doc->DoSelectProfile(p_event.GetRow() + 1); + if (p_event.GetCol() == -1 && p_event.GetRow() == -1) { + // The corner label: restore the order in which the profiles were computed + p_event.Veto(); + m_sortOrder.Reset(); + OnUpdate(); + } + else if (p_event.GetCol() == -1) { + m_doc->DoSelectProfile(m_sortOrder.GetProfile(p_event.GetRow())); + } + else if (p_event.GetRow() == -1) { + // A strategy: sort the profiles on the probability it is played with + p_event.Veto(); + m_sortOrder.ToggleColumn(p_event.GetCol() + 1); + OnUpdate(); } ClearSelection(); @@ -68,7 +103,7 @@ void MixedStrategyProfileList::OnLabelClick(wxGridEvent &p_event) void MixedStrategyProfileList::OnCellClick(wxGridEvent &p_event) { - m_doc->DoSelectProfile(p_event.GetRow() + 1); + m_doc->DoSelectProfile(m_sortOrder.GetProfile(p_event.GetRow())); ClearSelection(); } @@ -126,9 +161,11 @@ void MixedStrategyProfileList::ResizeGrid(int p_rows, int p_cols) void MixedStrategyProfileList::UpdateLabels() { + // Row labels identify the profile itself, so that a profile keeps its + // number however the list happens to be sorted. for (int row = 0; row < GetNumberRows(); ++row) { wxString label; - label << (row + 1); + label << m_sortOrder.GetProfile(row); SetRowLabelValue(row, label); } @@ -136,7 +173,8 @@ void MixedStrategyProfileList::UpdateLabels() for (const auto &player : m_doc->GetGame()->GetPlayers()) { for (const auto &strategy : player->GetStrategies()) { wxString label; - label << player->GetNumber() << ": " << strategy->GetLabel(); + label << player->GetNumber() << ": " << strategy->GetLabel() + << wxString::FromUTF8(m_sortOrder.GetColumnMarker(index + 1)); SetColLabelValue(index++, label); } } @@ -150,7 +188,7 @@ void MixedStrategyProfileList::UpdateCells() const wxFont boldFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD); for (int row = 0; row < GetNumberRows(); ++row) { - const int profile = row + 1; + const int profile = m_sortOrder.GetProfile(row); for (int col = 0; col < GetNumberCols(); ++col) { SetCellValue( @@ -169,6 +207,13 @@ void MixedStrategyProfileList::UpdateCells() } } +void MixedStrategyProfileList::UpdateHighlights() +{ + // GetColumn() numbers columns from one, and is zero when unsorted + m_labels->SetSortedColumn(m_sortOrder.GetColumn() - 1); + m_labels->SetSelectedRow(m_sortOrder.GetRow(m_doc->GetWorkspace().GetCurrentProfile())); +} + void MixedStrategyProfileList::OnUpdate() { if (m_doc->GetWorkspace().NumProfileLists() == 0) { @@ -189,6 +234,10 @@ void MixedStrategyProfileList::OnUpdate() const int newCols = m_doc->GetGame()->GetStrategies().size(); ResizeGrid(newRows, newCols); + m_sortOrder.Rebuild(newRows, newCols, [&](int p_col, int p_left, int p_right) { + return profiles.CompareStrategyProb(p_col, p_left, p_right); + }); + UpdateHighlights(); UpdateLabels(); UpdateCells(); diff --git a/src/gui/nfgprofile.h b/src/gui/nfgprofile.h index 76d708ea7..4f5338f91 100644 --- a/src/gui/nfgprofile.h +++ b/src/gui/nfgprofile.h @@ -26,9 +26,17 @@ #include #include "gamedoc.h" +#include "profilelabels.h" +#include "profilesort.h" namespace Gambit::GUI { class MixedStrategyProfileList final : public wxGrid, public GameView { + /// The order in which the profiles are listed + ProfileSortOrder m_sortOrder; + /// Highlights the sorted column and the selected profile's row; owned by + /// the grid's table, which deletes it + ProfileLabelProvider *m_labels{nullptr}; + // Event handlers void OnLabelClick(wxGridEvent &); void OnCellClick(wxGridEvent &); @@ -36,6 +44,7 @@ class MixedStrategyProfileList final : public wxGrid, public GameView { void ResizeGrid(int p_rows, int p_cols); void UpdateLabels(); + void UpdateHighlights(); void UpdateCells(); // Overriding GameView members diff --git a/src/gui/profilelabels.cc b/src/gui/profilelabels.cc new file mode 100644 index 000000000..896675493 --- /dev/null +++ b/src/gui/profilelabels.cc @@ -0,0 +1,75 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/profilelabels.cc +// Highlighting of the row and column labels in the profile list windows +// +// 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 +#ifndef WX_PRECOMP +#include +#endif // WX_PRECOMP + +#include "profilelabels.h" + +namespace Gambit::GUI { + +namespace { + +//! +//! A header label renderer which fills the label with the grid's selection +//! colour before drawing it as usual. wxGrid calls DrawBorder() for an +//! ordinary label and DrawHighlighted() for the label of the row or column +//! the grid cursor is on; as the profile lists suppress the latter, the +//! highlighting is done here. +//! +template class HighlightedLabel final : public Base { +public: + void DrawBorder(const wxGrid &p_grid, wxDC &p_dc, wxRect &p_rect) const override + { + const wxColour background = p_grid.GetSelectionBackground(); + p_dc.SetPen(*wxTRANSPARENT_PEN); + p_dc.SetBrush(wxBrush(background.ChangeLightness(130))); + p_dc.DrawRectangle(p_rect); + Base::DrawBorder(p_grid, p_dc, p_rect); + } +}; + +// These are stateless, so one instance of each serves every profile list. +const HighlightedLabel s_highlightedColumn; +const HighlightedLabel s_highlightedRow; + +} // end anonymous namespace + +const wxGridColumnHeaderRenderer &ProfileLabelProvider::GetColumnHeaderRenderer(int p_col) +{ + if (p_col == m_col) { + return s_highlightedColumn; + } + return wxGridCellAttrProvider::GetColumnHeaderRenderer(p_col); +} + +const wxGridRowHeaderRenderer &ProfileLabelProvider::GetRowHeaderRenderer(int p_row) +{ + if (p_row == m_row) { + return s_highlightedRow; + } + return wxGridCellAttrProvider::GetRowHeaderRenderer(p_row); +} + +} // namespace Gambit::GUI diff --git a/src/gui/profilelabels.h b/src/gui/profilelabels.h new file mode 100644 index 000000000..ff6f8a13d --- /dev/null +++ b/src/gui/profilelabels.h @@ -0,0 +1,62 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/profilelabels.h +// Highlighting of the row and column labels in the profile list windows +// +// 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_PROFILELABELS_H +#define GAMBIT_GUI_PROFILELABELS_H + +#include + +namespace Gambit::GUI { + +//! +//! Draws the labels of a profile list, highlighting the column the list is +//! sorted on and the row showing the currently selected profile. +//! +//! Left to itself, wxGrid highlights the labels of the row and column the +//! grid cursor happens to be on. The cursor carries no meaning in a profile +//! list, and cannot be moved out of the way, as wxGrid places it back on the +//! first cell whenever the grid is repainted. The profile lists therefore +//! turn that highlighting off with DisableOverlaySelection(), and use this +//! provider to highlight the labels which do carry meaning instead. +//! +//! Rows and columns are numbered from zero, following the convention of +//! wxGrid. An instance is owned by the grid's table, which deletes it. +//! +class ProfileLabelProvider final : public wxGridCellAttrProvider { +public: + /// Highlight the label of column p_col, or of no column if -1 + void SetSortedColumn(int p_col) { m_col = p_col; } + + /// Highlight the label of row p_row, or of no row if -1 + void SetSelectedRow(int p_row) { m_row = p_row; } + + const wxGridColumnHeaderRenderer &GetColumnHeaderRenderer(int p_col) override; + const wxGridRowHeaderRenderer &GetRowHeaderRenderer(int p_row) override; + +private: + int m_col{-1}; + int m_row{-1}; +}; + +} // namespace Gambit::GUI + +#endif // GAMBIT_GUI_PROFILELABELS_H diff --git a/src/gui/profilesort.cc b/src/gui/profilesort.cc new file mode 100644 index 000000000..f3ee7f7a0 --- /dev/null +++ b/src/gui/profilesort.cc @@ -0,0 +1,103 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/profilesort.cc +// Ordering of profiles in the profile list windows +// +// 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 "profilesort.h" + +namespace Gambit::GUI { + +void ProfileSortOrder::ToggleColumn(int p_col) +{ + if (p_col == m_column) { + m_ascending = !m_ascending; + } + else { + m_column = p_col; + m_ascending = true; + } +} + +void ProfileSortOrder::Reset() +{ + m_column = 0; + m_ascending = true; +} + +void ProfileSortOrder::Rebuild(int p_numProfiles, int p_numCols, const CompareFunc &p_compare) +{ + m_profiles.resize(p_numProfiles); + std::iota(m_profiles.begin(), m_profiles.end(), 1); + + if (m_column < 1 || m_column > p_numCols) { + // Either the list is unsorted, or the column sorted on is no longer + // part of the game; either way, show the profiles as computed. + m_column = 0; + m_ascending = true; + return; + } + + // Order on the sort column first, then on the profile as a whole, taking + // the columns from left to right. std::stable_sort leaves profiles which + // compare equal throughout in the order in which they were computed, in + // both sort directions. + std::stable_sort(m_profiles.begin(), m_profiles.end(), [&](int p_left, int p_right) { + const int direction = m_ascending ? 1 : -1; + + const int onSortColumn = p_compare(m_column, p_left, p_right); + if (onSortColumn != 0) { + return onSortColumn * direction < 0; + } + for (int col = 1; col <= p_numCols; ++col) { + const int onColumn = p_compare(col, p_left, p_right); + if (onColumn != 0) { + return onColumn * direction < 0; + } + } + return false; + }); +} + +int ProfileSortOrder::GetProfile(int p_row) const +{ + if (p_row < 0 || p_row >= static_cast(m_profiles.size())) { + return 0; + } + return m_profiles[p_row]; +} + +int ProfileSortOrder::GetRow(int p_profile) const +{ + const auto row = std::find(m_profiles.begin(), m_profiles.end(), p_profile); + return (row == m_profiles.end()) ? -1 : static_cast(row - m_profiles.begin()); +} + +const char *ProfileSortOrder::GetColumnMarker(int p_col) const +{ + if (p_col != m_column) { + return ""; + } + return m_ascending ? " ▲" : " ▼"; +} + +} // namespace Gambit::GUI diff --git a/src/gui/profilesort.h b/src/gui/profilesort.h new file mode 100644 index 000000000..ca53f3059 --- /dev/null +++ b/src/gui/profilesort.h @@ -0,0 +1,98 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/profilesort.h +// Ordering of profiles in the profile list windows +// +// 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_PROFILESORT_H +#define GAMBIT_GUI_PROFILESORT_H + +#include +#include + +namespace Gambit::GUI { + +//! +//! Maintains the order in which the profiles of an analysis output are +//! displayed in a profile list window. +//! +//! Profiles and columns are both numbered from one, following the +//! convention of the analysis classes; display rows are numbered from +//! zero, following the convention of wxGrid. With no sort column set, +//! profiles are shown in the order in which they were computed. +//! +//! Sorting is on the probability in the sort column, with ties broken by +//! comparing the profiles entry-by-entry from the first column onwards, +//! and any remaining ties broken by the order of computation. Sorting on +//! the first column therefore orders the list lexicographically by +//! profile. Entries which are not defined for a profile (for instance, +//! the behavior at an information set which the profile does not reach) +//! sort after all defined entries. +//! +class ProfileSortOrder { +public: + /// Compares the entries in column p_col of two profiles, returning a + /// negative value, zero, or a positive value as the entry of p_left is + /// less than, equal to, or greater than that of p_right. Comparing is + /// left to the caller so that entries are ordered in their own type, + /// exactly, rather than through a common numeric type. + using CompareFunc = std::function; + + /// @name Selecting the sort + //@{ + /// Sort on p_col; selecting the current sort column reverses the direction + void ToggleColumn(int p_col); + + /// Restore the order in which the profiles were computed + void Reset(); + + /// The column being sorted on, or zero if the list is unsorted + int GetColumn() const { return m_column; } + + /// Is the sort in ascending order? + bool IsAscending() const { return m_ascending; } + //@} + + /// @name Applying the sort + //@{ + /// Recompute the ordering of p_numProfiles profiles of p_numCols entries + void Rebuild(int p_numProfiles, int p_numCols, const CompareFunc &p_compare); + + /// The profile displayed in row p_row, or zero if there is no such row + int GetProfile(int p_row) const; + + /// The row in which p_profile is displayed, or -1 if there is no such profile + int GetRow(int p_profile) const; + + /// The marker to append to the label of column p_col, as UTF-8: an arrow + /// showing the direction of the sort on the column being sorted on, and + /// empty for every other column. + const char *GetColumnMarker(int p_col) const; + //@} + +private: + int m_column{0}; + bool m_ascending{true}; + /// The profile shown in each row, in display order + std::vector m_profiles; +}; + +} // namespace Gambit::GUI + +#endif // GAMBIT_GUI_PROFILESORT_H