From 7539e9153296bdc443045d7841f8b99870725dc5 Mon Sep 17 00:00:00 2001 From: Amelie Kleber Date: Mon, 24 Aug 2026 11:29:41 +0200 Subject: [PATCH 1/5] add sorting functionality for equilibria in the GUI --- Makefile.am | 2 + doc/gui.nash.rst | 19 ++++++ src/gui/analysis.cc | 30 +++++++++ src/gui/analysis.h | 8 +++ src/gui/efgprofile.cc | 43 ++++++++++--- src/gui/efgprofile.h | 4 ++ src/gui/nfgprofile.cc | 40 ++++++++++-- src/gui/nfgprofile.h | 4 ++ src/gui/profilesort.cc | 134 +++++++++++++++++++++++++++++++++++++++++ src/gui/profilesort.h | 96 +++++++++++++++++++++++++++++ 10 files changed, 367 insertions(+), 13 deletions(-) create mode 100644 src/gui/profilesort.cc create mode 100644 src/gui/profilesort.h diff --git a/Makefile.am b/Makefile.am index 1bda863ff..b64b528b6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -504,6 +504,8 @@ gambit_SOURCES = \ src/gui/nfgprofile.h \ src/gui/nfgtable.cc \ src/gui/nfgtable.h \ + src/gui/profilesort.cc \ + src/gui/profilesort.h \ src/gui/renratio.cc \ src/gui/renratio.h \ src/gui/style.cc \ diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index 2fee7672a..425760fab 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -149,6 +149,25 @@ the screenshot, it is set to 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. Whenever more than one equilibrium has been found, each +column label in the profiles listing carries a small +:guilabel:`↕` marker, indicating that the list can be sorted on that +column. Clicking on the label of a column 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 marker on the +column sorted on becomes 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. + The currently selected equilibrium is shown in bold in the profiles listing, and information about this equilibrium is displayed in the extensive game. In the figure, the probabilities of selecting each diff --git a/src/gui/analysis.cc b/src/gui/analysis.cc index 08b2b35dc..335eb3f4a 100644 --- a/src/gui/analysis.cc +++ b/src/gui/analysis.cc @@ -346,6 +346,36 @@ std::string AnalysisProfileList::GetStrategyValue(int p_strategy, int p_index } } +template +std::optional AnalysisProfileList::GetActionProbValue(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 static_cast(profile[p_action]); + } + catch (std::out_of_range &) { + return {}; + } +} + +template +std::optional AnalysisProfileList::GetStrategyProbValue(int p_strategy, + int p_index) const +{ + try { + const MixedStrategyProfile &profile = *m_mixedProfiles[p_index]; + return static_cast(profile[p_strategy]); + } + catch (std::out_of_range &) { + return {}; + } +} + template LegacyWorkspaceFile::Analysis AnalysisProfileList::Save() const { LegacyWorkspaceFile::Analysis result; diff --git a/src/gui/analysis.h b/src/gui/analysis.h index c7e9d72f7..bed0e383d 100644 --- a/src/gui/analysis.h +++ b/src/gui/analysis.h @@ -115,6 +115,12 @@ 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; + /// Get the probability of an action as a number, for ordering profiles; + /// empty if the profile does not define behavior at the information set + virtual std::optional GetActionProbValue(int p_action, int p_index) const = 0; + /// Get the probability of a strategy as a number, for ordering profiles + virtual std::optional GetStrategyProbValue(int p_strategy, int p_index) const = 0; + /// Map all behavior profiles to corresponding mixed profiles virtual void BuildNfg() = 0; @@ -173,6 +179,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; + std::optional GetActionProbValue(int p_action, int p_index) const override; + std::optional GetStrategyProbValue(int p_strategy, int p_index) const override; /// Get the index of the currently selected profile int GetCurrent() const override { return m_current; } diff --git a/src/gui/efgprofile.cc b/src/gui/efgprofile.cc index 43336acfe..0eb59beb4 100644 --- a/src/gui/efgprofile.cc +++ b/src/gui/efgprofile.cc @@ -50,6 +50,15 @@ MixedBehaviorProfileList::MixedBehaviorProfileList(wxWindow *p_parent, SetCellHighlightPenWidth(0); SetCellHighlightROPenWidth(0); + // 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 +68,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 +89,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 +124,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 +136,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 +150,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); @@ -176,6 +202,9 @@ void MixedBehaviorProfileList::OnUpdate() BeginBatch(); ResizeGrid(profiles.NumProfiles(), profileLength); + m_sortOrder.Rebuild(profiles.NumProfiles(), profileLength, [&](int p_col, int p_profile) { + return profiles.GetActionProbValue(p_col, p_profile); + }); UpdateLabels(); UpdateCells(); diff --git a/src/gui/efgprofile.h b/src/gui/efgprofile.h index f21925bc9..fa804cc12 100644 --- a/src/gui/efgprofile.h +++ b/src/gui/efgprofile.h @@ -26,9 +26,13 @@ #include #include "gamedoc.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; + // Event handlers void OnLabelClick(wxGridEvent &); void OnCellClick(wxGridEvent &); diff --git a/src/gui/nfgprofile.cc b/src/gui/nfgprofile.cc index 855b8990f..4e7c3c782 100644 --- a/src/gui/nfgprofile.cc +++ b/src/gui/nfgprofile.cc @@ -50,6 +50,16 @@ MixedStrategyProfileList::MixedStrategyProfileList(wxWindow *p_parent, SetCellHighlightPenWidth(0); SetCellHighlightROPenWidth(0); + // 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 +69,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 +90,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 +148,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 +160,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 +175,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( @@ -189,6 +214,9 @@ void MixedStrategyProfileList::OnUpdate() const int newCols = m_doc->GetGame()->GetStrategies().size(); ResizeGrid(newRows, newCols); + m_sortOrder.Rebuild(newRows, newCols, [&](int p_col, int p_profile) { + return profiles.GetStrategyProbValue(p_col, p_profile); + }); UpdateLabels(); UpdateCells(); diff --git a/src/gui/nfgprofile.h b/src/gui/nfgprofile.h index 76d708ea7..52d657d34 100644 --- a/src/gui/nfgprofile.h +++ b/src/gui/nfgprofile.h @@ -26,9 +26,13 @@ #include #include "gamedoc.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; + // Event handlers void OnLabelClick(wxGridEvent &); void OnCellClick(wxGridEvent &); diff --git a/src/gui/profilesort.cc b/src/gui/profilesort.cc new file mode 100644 index 000000000..74b3d959e --- /dev/null +++ b/src/gui/profilesort.cc @@ -0,0 +1,134 @@ +// +// 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 { + +namespace { + +/// The sort key of a profile: the entry in the sort column, followed by +/// the entries of the profile in column order. Undefined entries are +/// represented by an empty optional, and sort after all defined entries. +using SortKey = std::vector>; + +bool EntryLess(const std::optional &p_left, const std::optional &p_right) +{ + if (!p_left.has_value()) { + return false; + } + if (!p_right.has_value()) { + return true; + } + return p_left.value() < p_right.value(); +} + +bool KeyLess(const SortKey &p_left, const SortKey &p_right) +{ + return std::lexicographical_compare(p_left.begin(), p_left.end(), p_right.begin(), p_right.end(), + EntryLess); +} + +} // end anonymous namespace + +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 EntryFunc &p_entry) +{ + 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; + } + + std::vector keys(p_numProfiles); + for (int profile = 1; profile <= p_numProfiles; ++profile) { + SortKey &key = keys[profile - 1]; + key.reserve(p_numCols + 1); + key.push_back(p_entry(m_column, profile)); + for (int col = 1; col <= p_numCols; ++col) { + key.push_back(p_entry(col, profile)); + } + } + + // std::stable_sort keeps profiles whose keys are equal 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 SortKey &left = keys[p_left - 1]; + const SortKey &right = keys[p_right - 1]; + return m_ascending ? KeyLess(left, right) : KeyLess(right, left); + }); +} + +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 (m_profiles.size() < 2) { + // With at most one profile there is nothing to sort, so offering to + // sort would only be a distraction. + return ""; + } + if (p_col != m_column) { + // A light marker on the other columns, so that it is apparent that + // the list can be sorted on any of them. + 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..8cc27476a --- /dev/null +++ b/src/gui/profilesort.h @@ -0,0 +1,96 @@ +// +// 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 +#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: + /// Returns the entry in column p_col of profile p_profile, if defined + using EntryFunc = std::function(int p_col, int p_profile)>; + + /// @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 EntryFunc &p_entry); + + /// 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 a + /// hint that sorting is possible on the others. Empty when there are + /// fewer than two profiles, as there is then nothing to sort. + 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 From a4ab14f3851eba0e8c0e589681cab35d3945c73c Mon Sep 17 00:00:00 2001 From: Amelie Kleber Date: Mon, 24 Aug 2026 14:22:34 +0200 Subject: [PATCH 2/5] fix light blue highlight to be dynamic --- Makefile.am | 2 ++ src/gui/efgprofile.cc | 18 ++++++++++ src/gui/efgprofile.h | 5 +++ src/gui/nfgprofile.cc | 18 ++++++++++ src/gui/nfgprofile.h | 5 +++ src/gui/profilelabels.cc | 75 ++++++++++++++++++++++++++++++++++++++++ src/gui/profilelabels.h | 62 +++++++++++++++++++++++++++++++++ 7 files changed, 185 insertions(+) create mode 100644 src/gui/profilelabels.cc create mode 100644 src/gui/profilelabels.h diff --git a/Makefile.am b/Makefile.am index b64b528b6..79f19c072 100644 --- a/Makefile.am +++ b/Makefile.am @@ -504,6 +504,8 @@ 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 \ diff --git a/src/gui/efgprofile.cc b/src/gui/efgprofile.cc index 0eb59beb4..d8e64a3a2 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,12 @@ MixedBehaviorProfileList::MixedBehaviorProfileList(wxWindow *p_parent, SetCellHighlightPenWidth(0); SetCellHighlightROPenWidth(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. + DisableOverlaySelection(); + // The column and corner labels sort the list, so show them as clickable GetGridColLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); GetGridCornerLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); @@ -184,6 +194,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) { @@ -205,6 +222,7 @@ void MixedBehaviorProfileList::OnUpdate() m_sortOrder.Rebuild(profiles.NumProfiles(), profileLength, [&](int p_col, int p_profile) { return profiles.GetActionProbValue(p_col, p_profile); }); + UpdateHighlights(); UpdateLabels(); UpdateCells(); diff --git a/src/gui/efgprofile.h b/src/gui/efgprofile.h index fa804cc12..7713deed7 100644 --- a/src/gui/efgprofile.h +++ b/src/gui/efgprofile.h @@ -26,12 +26,16 @@ #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 &); @@ -40,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 4e7c3c782..76617f2c0 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,12 @@ MixedStrategyProfileList::MixedStrategyProfileList(wxWindow *p_parent, SetCellHighlightPenWidth(0); SetCellHighlightROPenWidth(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. + DisableOverlaySelection(); + // The column and corner labels sort the list, so show them as clickable GetGridColLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); GetGridCornerLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND)); @@ -194,6 +204,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) { @@ -217,6 +234,7 @@ void MixedStrategyProfileList::OnUpdate() m_sortOrder.Rebuild(newRows, newCols, [&](int p_col, int p_profile) { return profiles.GetStrategyProbValue(p_col, p_profile); }); + UpdateHighlights(); UpdateLabels(); UpdateCells(); diff --git a/src/gui/nfgprofile.h b/src/gui/nfgprofile.h index 52d657d34..4f5338f91 100644 --- a/src/gui/nfgprofile.h +++ b/src/gui/nfgprofile.h @@ -26,12 +26,16 @@ #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 &); @@ -40,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 From 24b1bda25972df313dd394d7fe31ff670d637746 Mon Sep 17 00:00:00 2001 From: Amelie Kleber Date: Mon, 24 Aug 2026 15:10:57 +0200 Subject: [PATCH 3/5] fix version mismatch --- src/gui/efgprofile.cc | 3 +++ src/gui/nfgprofile.cc | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/gui/efgprofile.cc b/src/gui/efgprofile.cc index d8e64a3a2..b1c92bf10 100644 --- a/src/gui/efgprofile.cc +++ b/src/gui/efgprofile.cc @@ -54,11 +54,14 @@ 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)); diff --git a/src/gui/nfgprofile.cc b/src/gui/nfgprofile.cc index 76617f2c0..d27a2c90f 100644 --- a/src/gui/nfgprofile.cc +++ b/src/gui/nfgprofile.cc @@ -54,11 +54,14 @@ 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)); From d5bf55c97a57ad0036951b41d43f57b056dd5578 Mon Sep 17 00:00:00 2001 From: Amelie Kleber Date: Fri, 28 Aug 2026 16:53:42 +0200 Subject: [PATCH 4/5] remove sorting indicator, sort as rationals --- doc/gui.nash.rst | 14 ++++----- src/gui/analysis.cc | 49 ++++++++++++++++++++++++++---- src/gui/analysis.h | 25 +++++++++++----- src/gui/efgprofile.cc | 7 +++-- src/gui/nfgprofile.cc | 4 +-- src/gui/profilesort.cc | 67 ++++++++++++------------------------------ src/gui/profilesort.h | 16 +++++----- 7 files changed, 100 insertions(+), 82 deletions(-) diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index 425760fab..9ab839d94 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -150,14 +150,12 @@ 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. Whenever more than one equilibrium has been found, each -column label in the profiles listing carries a small -:guilabel:`↕` marker, indicating that the list can be sorted on that -column. Clicking on the label of a column 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 marker on the -column sorted on becomes an arrow, :guilabel:`▲` or :guilabel:`▼`, -showing the direction of the sort. Equilibria which agree on that +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 diff --git a/src/gui/analysis.cc b/src/gui/analysis.cc index 335eb3f4a..fe0f49627 100644 --- a/src/gui/analysis.cc +++ b/src/gui/analysis.cc @@ -346,8 +346,33 @@ 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::GetActionProbValue(int p_action, int p_index) const +std::optional AnalysisProfileList::GetActionProbEntry(int p_action, int p_index) const { try { const MixedBehaviorProfile &profile = *m_behavProfiles[p_index]; @@ -356,7 +381,7 @@ std::optional AnalysisProfileList::GetActionProbValue(int p_action, i return {}; } - return static_cast(profile[p_action]); + return profile[p_action]; } catch (std::out_of_range &) { return {}; @@ -364,18 +389,30 @@ std::optional AnalysisProfileList::GetActionProbValue(int p_action, i } template -std::optional AnalysisProfileList::GetStrategyProbValue(int p_strategy, - int p_index) const +std::optional AnalysisProfileList::GetStrategyProbEntry(int p_strategy, int p_index) const { try { - const MixedStrategyProfile &profile = *m_mixedProfiles[p_index]; - return static_cast(profile[p_strategy]); + 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 bed0e383d..1114f42cd 100644 --- a/src/gui/analysis.h +++ b/src/gui/analysis.h @@ -115,11 +115,15 @@ 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; - /// Get the probability of an action as a number, for ordering profiles; - /// empty if the profile does not define behavior at the information set - virtual std::optional GetActionProbValue(int p_action, int p_index) const = 0; - /// Get the probability of a strategy as a number, for ordering profiles - virtual std::optional GetStrategyProbValue(int p_strategy, int p_index) 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; @@ -179,8 +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; - std::optional GetActionProbValue(int p_action, int p_index) const override; - std::optional GetStrategyProbValue(int p_strategy, int p_index) 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; } @@ -210,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 b1c92bf10..934510933 100644 --- a/src/gui/efgprofile.cc +++ b/src/gui/efgprofile.cc @@ -222,9 +222,10 @@ void MixedBehaviorProfileList::OnUpdate() BeginBatch(); ResizeGrid(profiles.NumProfiles(), profileLength); - m_sortOrder.Rebuild(profiles.NumProfiles(), profileLength, [&](int p_col, int p_profile) { - return profiles.GetActionProbValue(p_col, p_profile); - }); + 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/nfgprofile.cc b/src/gui/nfgprofile.cc index d27a2c90f..e43d599ce 100644 --- a/src/gui/nfgprofile.cc +++ b/src/gui/nfgprofile.cc @@ -234,8 +234,8 @@ void MixedStrategyProfileList::OnUpdate() const int newCols = m_doc->GetGame()->GetStrategies().size(); ResizeGrid(newRows, newCols); - m_sortOrder.Rebuild(newRows, newCols, [&](int p_col, int p_profile) { - return profiles.GetStrategyProbValue(p_col, p_profile); + 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(); diff --git a/src/gui/profilesort.cc b/src/gui/profilesort.cc index 74b3d959e..f3ee7f7a0 100644 --- a/src/gui/profilesort.cc +++ b/src/gui/profilesort.cc @@ -27,32 +27,6 @@ namespace Gambit::GUI { -namespace { - -/// The sort key of a profile: the entry in the sort column, followed by -/// the entries of the profile in column order. Undefined entries are -/// represented by an empty optional, and sort after all defined entries. -using SortKey = std::vector>; - -bool EntryLess(const std::optional &p_left, const std::optional &p_right) -{ - if (!p_left.has_value()) { - return false; - } - if (!p_right.has_value()) { - return true; - } - return p_left.value() < p_right.value(); -} - -bool KeyLess(const SortKey &p_left, const SortKey &p_right) -{ - return std::lexicographical_compare(p_left.begin(), p_left.end(), p_right.begin(), p_right.end(), - EntryLess); -} - -} // end anonymous namespace - void ProfileSortOrder::ToggleColumn(int p_col) { if (p_col == m_column) { @@ -70,7 +44,7 @@ void ProfileSortOrder::Reset() m_ascending = true; } -void ProfileSortOrder::Rebuild(int p_numProfiles, int p_numCols, const EntryFunc &p_entry) +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); @@ -83,22 +57,24 @@ void ProfileSortOrder::Rebuild(int p_numProfiles, int p_numCols, const EntryFunc return; } - std::vector keys(p_numProfiles); - for (int profile = 1; profile <= p_numProfiles; ++profile) { - SortKey &key = keys[profile - 1]; - key.reserve(p_numCols + 1); - key.push_back(p_entry(m_column, profile)); + // 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) { - key.push_back(p_entry(col, profile)); + const int onColumn = p_compare(col, p_left, p_right); + if (onColumn != 0) { + return onColumn * direction < 0; + } } - } - - // std::stable_sort keeps profiles whose keys are equal 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 SortKey &left = keys[p_left - 1]; - const SortKey &right = keys[p_right - 1]; - return m_ascending ? KeyLess(left, right) : KeyLess(right, left); + return false; }); } @@ -118,15 +94,8 @@ int ProfileSortOrder::GetRow(int p_profile) const const char *ProfileSortOrder::GetColumnMarker(int p_col) const { - if (m_profiles.size() < 2) { - // With at most one profile there is nothing to sort, so offering to - // sort would only be a distraction. - return ""; - } if (p_col != m_column) { - // A light marker on the other columns, so that it is apparent that - // the list can be sorted on any of them. - return " ↕"; + return ""; } return m_ascending ? " ▲" : " ▼"; } diff --git a/src/gui/profilesort.h b/src/gui/profilesort.h index 8cc27476a..ca53f3059 100644 --- a/src/gui/profilesort.h +++ b/src/gui/profilesort.h @@ -24,7 +24,6 @@ #define GAMBIT_GUI_PROFILESORT_H #include -#include #include namespace Gambit::GUI { @@ -48,8 +47,12 @@ namespace Gambit::GUI { //! class ProfileSortOrder { public: - /// Returns the entry in column p_col of profile p_profile, if defined - using EntryFunc = std::function(int p_col, int p_profile)>; + /// 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 //@{ @@ -69,7 +72,7 @@ class ProfileSortOrder { /// @name Applying the sort //@{ /// Recompute the ordering of p_numProfiles profiles of p_numCols entries - void Rebuild(int p_numProfiles, int p_numCols, const EntryFunc &p_entry); + 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; @@ -78,9 +81,8 @@ class ProfileSortOrder { 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 a - /// hint that sorting is possible on the others. Empty when there are - /// fewer than two profiles, as there is then nothing to sort. + /// 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; //@} From 4bda5496c1c8dd5d4dc7a419baf2b2d0b01e0289 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 31 Aug 2026 17:30:13 +0100 Subject: [PATCH 5/5] Update ChangeLog and contributors --- .all-contributorsrc | 3 ++- ChangeLog | 1 + README.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) 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 cf2523b45..1217dc8b7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,7 @@ `logit_estimate`, `ipa_solve`, and `gnm_solve` now also accept a rational-precision profile directly (as a starting point or perturbation vector), converting it internally. (#721, #458) +- In the GUI, computed strategy and behaviour profiles can now be sorted by probability (#1077) ### Changed - Command-line tools are now implemented in `pygambit` rather than C++-based compiled programs. 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

⚠️ 💻