From 9a7f1bb7e19cb7efa227b987b7a44d6db027f00a Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 15:24:45 +0100 Subject: [PATCH 01/12] Introduce context menu to determine drag-and-drop operation --- src/gui/efgdisplay.cc | 143 ++++++++++++++++++++++++++++-------------- src/gui/efgdisplay.h | 3 + 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 5ab177bc7..1bad50a18 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -119,6 +119,7 @@ class PlayerDropTarget : public wxTextDropTarget { bool OnDropSetOutcome(const GameNode &p_node, const wxString &p_text); bool OnDropMoveOutcome(const GameNode &p_node, const wxString &p_text); bool OnDropCopyOutcome(const GameNode &p_node, const wxString &p_text); + bool OnDropTreeNode(const GameNode &p_node, const wxString &p_text, const wxPoint &p_pos); public: explicit PlayerDropTarget(EfgDisplay *p_owner) @@ -255,6 +256,20 @@ bool PlayerDropTarget::OnDropCopyOutcome(const GameNode &p_node, const wxString return true; } +bool PlayerDropTarget::OnDropTreeNode(const GameNode &p_node, const wxString &p_text, + const wxPoint &p_pos) +{ + long n; + p_text.Right(p_text.Length() - 1).ToLong(&n); + + const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); + if (!srcNode || srcNode == p_node || srcNode->IsTerminal()) { + return false; + } + + return m_owner->ShowTreeDropMenu(p_node, srcNode, p_pos); +} + bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_text) { const Game efg = m_owner->GetDocument()->GetGame(); @@ -281,6 +296,8 @@ bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_te try { switch (static_cast(p_text[0])) { + case 'N': + return OnDropTreeNode(node, p_text, wxPoint(p_x, p_y)); case 'P': return OnDropPlayer(node, p_text); case 'C': @@ -361,6 +378,75 @@ void EfgDisplay::MakeMenus() m_nodeMenu->Append(GBT_MENU_EDIT_GAME, _("&Game properties"), _("Edit properties of the game")); } +bool EfgDisplay::ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, + const wxPoint &p_pos) +{ + if (!p_targetNode || !p_sourceNode || p_sourceNode->IsTerminal()) { + return false; + } + + const bool canCopyOrMoveTree = p_targetNode->IsTerminal(); + const bool canUseSameInfoset = + (!p_targetNode->IsTerminal() && + p_targetNode->GetChildren().size() == p_sourceNode->GetChildren().size()) || + p_targetNode->IsTerminal(); + + if (!canCopyOrMoveTree && !canUseSameInfoset) { + return false; + } + + const int copyTreeId = wxWindow::NewControlId(); + const int moveTreeId = wxWindow::NewControlId(); + const int infosetId = wxWindow::NewControlId(); + + wxMenu menu; + + if (canCopyOrMoveTree) { + menu.Append(copyTreeId, _("Copy subtree here")); + menu.Append(moveTreeId, _("Move subtree here")); + } + + if (canUseSameInfoset) { + if (!menu.GetMenuItems().empty()) { + menu.AppendSeparator(); + } + + if (p_targetNode->IsTerminal()) { + menu.Append(infosetId, _("Insert move using same information set")); + } + else { + menu.Append(infosetId, _("Put node in same information set")); + } + } + + const int selection = GetPopupMenuSelectionFromUser(menu, p_pos); + + try { + if (selection == copyTreeId) { + m_doc->DoCopyTree(p_targetNode, p_sourceNode); + return true; + } + if (selection == moveTreeId) { + m_doc->DoMoveTree(p_targetNode, p_sourceNode); + return true; + } + if (selection == infosetId) { + if (!p_targetNode->IsTerminal()) { + m_doc->DoSetInfoset(p_targetNode, p_sourceNode->GetInfoset()); + } + else { + m_doc->DoAppendMove(p_targetNode, p_sourceNode->GetInfoset()); + } + return true; + } + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } + + return false; +} + //--------------------------------------------------------------------- // EfgDisplay: Event-hook members //--------------------------------------------------------------------- @@ -977,61 +1063,22 @@ void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) GameNode node = m_layout.NodeHitTest(x, y); if (node && !node->IsTerminal()) { - const GamePlayer player = node->GetPlayer(); - if (p_event.ControlDown()) { - // Copy subtree - const wxBitmap bitmap(tree_xpm); + const wxBitmap bitmap(tree_xpm); #if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - - wxString label; - label << "C" << node->GetNumber(); - wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(true); - } - else if (p_event.ShiftDown()) { - // Copy move (information set) - // This should be the pawn icon! - const wxBitmap bitmap(move_xpm); -#if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - - wxString label; - label << "I" << node->GetNumber(); - wxTextDataObject textData(label); - - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(wxDrag_DefaultMove); - } - else { - // Move subtree - const wxBitmap bitmap(tree_xpm); -#if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); + const auto image = wxCursor(bitmap.ConvertToImage()); #else - wxIcon image; - image.CopyFromBitmap(bitmap); + wxIcon image; + image.CopyFromBitmap(bitmap); #endif // _WXMSW__ - wxString label; - label << "M" << node->GetNumber(); - wxTextDataObject textData(label); + wxString label; + label << "N" << node->GetNumber(); + wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(wxDrag_DefaultMove); - } + wxDropSource source(textData, this, image, image, image); + /*wxDragResult result =*/source.DoDragDrop(wxDrag_DefaultMove); return; } - node = m_layout.OutcomeHitTest(x, y); if (node && node->GetOutcome()) { diff --git a/src/gui/efgdisplay.h b/src/gui/efgdisplay.h index 3f10e8d69..7ef2d7fcd 100644 --- a/src/gui/efgdisplay.h +++ b/src/gui/efgdisplay.h @@ -109,6 +109,9 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { void EnsureNodeVisible(const GameNode &); + bool ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, + const wxPoint &p_pos); + DECLARE_EVENT_TABLE() }; } // namespace Gambit::GUI From 82a6ec231c4ec2a8830733f549fb7ad7e791745c Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 15:27:47 +0100 Subject: [PATCH 02/12] Remove unused code --- src/gui/efgdisplay.cc | 59 ------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 1bad50a18..6b615485d 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -113,9 +113,6 @@ class PlayerDropTarget : public wxTextDropTarget { GameDocument *m_model; bool OnDropPlayer(const GameNode &p_node, const wxString &p_text); - bool OnDropCopyNode(const GameNode &p_node, const wxString &p_text); - bool OnDropMoveNode(const GameNode &p_node, const wxString &p_text); - bool OnDropInfoset(const GameNode &p_node, const wxString &p_text); bool OnDropSetOutcome(const GameNode &p_node, const wxString &p_text); bool OnDropMoveOutcome(const GameNode &p_node, const wxString &p_text); bool OnDropCopyOutcome(const GameNode &p_node, const wxString &p_text); @@ -170,55 +167,6 @@ bool PlayerDropTarget::OnDropPlayer(const GameNode &p_node, const wxString &p_te return true; } -bool PlayerDropTarget::OnDropCopyNode(const GameNode &p_node, const wxString &p_text) -{ - long n; - p_text.Right(p_text.Length() - 1).ToLong(&n); - const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); - if (!srcNode) { - return false; - } - if (p_node->IsTerminal() && !srcNode->IsTerminal()) { - m_model->DoCopyTree(p_node, srcNode); - return true; - } - return false; -} - -bool PlayerDropTarget::OnDropMoveNode(const GameNode &p_node, const wxString &p_text) -{ - long n; - p_text.Right(p_text.Length() - 1).ToLong(&n); - const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); - if (!srcNode) { - return false; - } - if (p_node->IsTerminal() && !srcNode->IsTerminal()) { - m_model->DoMoveTree(p_node, srcNode); - return true; - } - return false; -} - -bool PlayerDropTarget::OnDropInfoset(const GameNode &p_node, const wxString &p_text) -{ - long n; - p_text.Right(p_text.Length() - 1).ToLong(&n); - const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); - if (!srcNode) { - return false; - } - if (!p_node->IsTerminal() && p_node->GetChildren().size() == srcNode->GetChildren().size()) { - m_model->DoSetInfoset(p_node, srcNode->GetInfoset()); - return true; - } - else if (p_node->IsTerminal() && !srcNode->IsTerminal()) { - m_model->DoAppendMove(p_node, srcNode->GetInfoset()); - return true; - } - return false; -} - bool PlayerDropTarget::OnDropSetOutcome(const GameNode &p_node, const wxString &p_text) { long n; @@ -300,12 +248,6 @@ bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_te return OnDropTreeNode(node, p_text, wxPoint(p_x, p_y)); case 'P': return OnDropPlayer(node, p_text); - case 'C': - return OnDropCopyNode(node, p_text); - case 'M': - return OnDropMoveNode(node, p_text); - case 'I': - return OnDropInfoset(node, p_text); case 'O': return OnDropSetOutcome(node, p_text); case 'o': @@ -1050,7 +992,6 @@ void EfgDisplay::OnMagnify(wxMouseEvent &p_event) } #include "bitmaps/tree.xpm" -#include "bitmaps/move.xpm" void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) { From 79b0d826590db8efe9d40933573cfdc500476ae9 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 15:47:39 +0100 Subject: [PATCH 03/12] Remove custom cursor for tree drags --- src/gui/efgdisplay.cc | 77 ++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 6b615485d..a37d4a576 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -223,16 +223,7 @@ bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_te const Game efg = m_owner->GetDocument()->GetGame(); int x, y; -#if defined(__WXMSW__) - // The +12 here is designed to effectively make the hot spot on - // the cursor the center of the cursor image (they're currently - // 24 pixels wide). - m_owner->CalcUnscrolledPosition(p_x + 12, p_y + 12, &x, &y); -#else - // Under GTK, there is an angle in the upper left-hand corner which - // serves to identify the hot spot. Thus, no adjustment is used m_owner->CalcUnscrolledPosition(p_x, p_y, &x, &y); -#endif // __WXMSW__ or defined(__WXMAC__) x = m_owner->DeviceToLayout(x); y = m_owner->DeviceToLayout(y); @@ -991,7 +982,61 @@ void EfgDisplay::OnMagnify(wxMouseEvent &p_event) } } -#include "bitmaps/tree.xpm" +namespace { + +wxCursor MakeTreeDragCursor() +{ + constexpr int width = 24; + constexpr int height = 24; + + wxBitmap bitmap(width, height, 32); + + { + wxMemoryDC dc(bitmap); + dc.SetBackground(*wxTRANSPARENT_BRUSH); + dc.Clear(); + + const wxColour stroke(70, 70, 70); + const wxColour fill(255, 255, 255); + + dc.SetPen(wxPen(stroke, 2)); + dc.SetBrush(wxBrush(fill, wxBRUSHSTYLE_SOLID)); + + // Simple subtree glyph: one parent node and two children. + dc.DrawCircle(7, 5, 3); + dc.DrawCircle(7, 18, 3); + dc.DrawCircle(18, 18, 3); + + dc.SetPen(wxPen(stroke, 1)); + dc.DrawLine(7, 8, 7, 15); + dc.DrawLine(10, 18, 15, 18); + + dc.SelectObject(wxNullBitmap); + } + + wxImage image = bitmap.ConvertToImage(); + if (image.HasAlpha()) { + // Good. + } + else { + image.InitAlpha(); + } + + image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_X, 0); + image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_Y, 0); + + return wxCursor(image); +} + +wxCursor MakeDragCursor(const wxBitmap &p_bitmap, int p_hotspotX, int p_hotspotY) +{ + wxImage image = p_bitmap.ConvertToImage(); + image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_X, p_hotspotX); + image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_Y, p_hotspotY); + return wxCursor(image); +} + +} // namespace void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) { @@ -1004,20 +1049,12 @@ void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) GameNode node = m_layout.NodeHitTest(x, y); if (node && !node->IsTerminal()) { - const wxBitmap bitmap(tree_xpm); -#if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - wxString label; label << "N" << node->GetNumber(); wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(wxDrag_DefaultMove); + wxDropSource source(textData, this); + source.DoDragDrop(wxDrag_DefaultMove); return; } node = m_layout.OutcomeHitTest(x, y); From 1501aa3ff447c98b3b2d309425200f32acf7a807 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 15:53:49 +0100 Subject: [PATCH 04/12] Simplify outcome DnD --- src/gui/efgdisplay.cc | 198 ++++++++++++------------------------------ src/gui/efgdisplay.h | 2 + 2 files changed, 57 insertions(+), 143 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index a37d4a576..bc5a523fa 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -81,29 +81,6 @@ void TreePayoffEditor::OnChar(wxKeyEvent &p_event) } } -//-------------------------------------------------------------------------- -// Bitmap drawing functions -//-------------------------------------------------------------------------- - -static wxBitmap MakeOutcomeBitmap() -{ - wxBitmap bitmap(24, 24); - wxMemoryDC dc; - dc.SelectObject(bitmap); - dc.Clear(); - dc.SetPen(wxPen(*wxBLACK, 1, wxPENSTYLE_SOLID)); - // Make a gold-colored background - dc.SetBrush(wxBrush(wxColour(255, 215, 0), wxBRUSHSTYLE_SOLID)); - dc.DrawCircle(12, 12, 10); - dc.SetFont(wxFont(12, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD)); - dc.SetTextForeground(wxColour(0, 192, 0)); - - int width, height; - dc.GetTextExtent(wxT("u"), &width, &height); - dc.DrawText(wxT("u"), 12 - width / 2, 12 - height / 2); - return bitmap; -} - //-------------------------------------------------------------------------- // class PlayerDropTarget //-------------------------------------------------------------------------- @@ -113,9 +90,7 @@ class PlayerDropTarget : public wxTextDropTarget { GameDocument *m_model; bool OnDropPlayer(const GameNode &p_node, const wxString &p_text); - bool OnDropSetOutcome(const GameNode &p_node, const wxString &p_text); - bool OnDropMoveOutcome(const GameNode &p_node, const wxString &p_text); - bool OnDropCopyOutcome(const GameNode &p_node, const wxString &p_text); + bool OnDropOutcome(const GameNode &p_node, const wxString &p_text, const wxPoint &p_pos); bool OnDropTreeNode(const GameNode &p_node, const wxString &p_text, const wxPoint &p_pos); public: @@ -167,41 +142,18 @@ bool PlayerDropTarget::OnDropPlayer(const GameNode &p_node, const wxString &p_te return true; } -bool PlayerDropTarget::OnDropSetOutcome(const GameNode &p_node, const wxString &p_text) +bool PlayerDropTarget::OnDropOutcome(const GameNode &p_node, const wxString &p_text, + const wxPoint &p_pos) { long n; p_text.Right(p_text.Length() - 1).ToLong(&n); - const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); - if (!srcNode || p_node == srcNode) { - return false; - } - m_model->DoSetOutcome(p_node, srcNode->GetOutcome()); - return true; -} -bool PlayerDropTarget::OnDropMoveOutcome(const GameNode &p_node, const wxString &p_text) -{ - long n; - p_text.Right(p_text.Length() - 1).ToLong(&n); const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); - if (!srcNode || p_node == srcNode) { + if (!srcNode || srcNode == p_node || !srcNode->GetOutcome()) { return false; } - m_model->DoSetOutcome(p_node, srcNode->GetOutcome()); - m_model->DoSetOutcome(srcNode, nullptr); - return true; -} -bool PlayerDropTarget::OnDropCopyOutcome(const GameNode &p_node, const wxString &p_text) -{ - long n; - p_text.Right(p_text.Length() - 1).ToLong(&n); - const GameNode srcNode = GetNode(m_model->GetGame()->GetRoot(), n); - if (!srcNode || p_node == srcNode) { - return false; - } - m_model->DoCopyOutcome(p_node, srcNode->GetOutcome()); - return true; + return m_owner->ShowOutcomeDropMenu(p_node, srcNode, p_pos); } bool PlayerDropTarget::OnDropTreeNode(const GameNode &p_node, const wxString &p_text, @@ -240,11 +192,7 @@ bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_te case 'P': return OnDropPlayer(node, p_text); case 'O': - return OnDropSetOutcome(node, p_text); - case 'o': - return OnDropMoveOutcome(node, p_text); - case 'p': - return OnDropCopyOutcome(node, p_text); + return OnDropOutcome(node, p_text, wxPoint(p_x, p_y)); default: return false; } @@ -380,6 +328,48 @@ bool EfgDisplay::ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode & return false; } +bool EfgDisplay::ShowOutcomeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, + const wxPoint &p_pos) +{ + if (!p_targetNode || !p_sourceNode || p_targetNode == p_sourceNode || + !p_sourceNode->GetOutcome()) { + return false; + } + + const int useSameOutcomeId = wxWindow::NewControlId(); + const int copyOutcomeId = wxWindow::NewControlId(); + const int moveOutcomeId = wxWindow::NewControlId(); + + wxMenu menu; + menu.Append(useSameOutcomeId, _("Use same outcome here")); + menu.Append(copyOutcomeId, _("Copy outcome here")); + menu.AppendSeparator(); + menu.Append(moveOutcomeId, _("Move outcome here")); + + const int selection = GetPopupMenuSelectionFromUser(menu, p_pos); + + try { + if (selection == useSameOutcomeId) { + m_doc->DoSetOutcome(p_targetNode, p_sourceNode->GetOutcome()); + return true; + } + if (selection == copyOutcomeId) { + m_doc->DoCopyOutcome(p_targetNode, p_sourceNode->GetOutcome()); + return true; + } + if (selection == moveOutcomeId) { + m_doc->DoSetOutcome(p_targetNode, p_sourceNode->GetOutcome()); + m_doc->DoSetOutcome(p_sourceNode, nullptr); + return true; + } + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } + + return false; +} + //--------------------------------------------------------------------- // EfgDisplay: Event-hook members //--------------------------------------------------------------------- @@ -982,62 +972,6 @@ void EfgDisplay::OnMagnify(wxMouseEvent &p_event) } } -namespace { - -wxCursor MakeTreeDragCursor() -{ - constexpr int width = 24; - constexpr int height = 24; - - wxBitmap bitmap(width, height, 32); - - { - wxMemoryDC dc(bitmap); - dc.SetBackground(*wxTRANSPARENT_BRUSH); - dc.Clear(); - - const wxColour stroke(70, 70, 70); - const wxColour fill(255, 255, 255); - - dc.SetPen(wxPen(stroke, 2)); - dc.SetBrush(wxBrush(fill, wxBRUSHSTYLE_SOLID)); - - // Simple subtree glyph: one parent node and two children. - dc.DrawCircle(7, 5, 3); - dc.DrawCircle(7, 18, 3); - dc.DrawCircle(18, 18, 3); - - dc.SetPen(wxPen(stroke, 1)); - dc.DrawLine(7, 8, 7, 15); - dc.DrawLine(10, 18, 15, 18); - - dc.SelectObject(wxNullBitmap); - } - - wxImage image = bitmap.ConvertToImage(); - if (image.HasAlpha()) { - // Good. - } - else { - image.InitAlpha(); - } - - image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_X, 0); - image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_Y, 0); - - return wxCursor(image); -} - -wxCursor MakeDragCursor(const wxBitmap &p_bitmap, int p_hotspotX, int p_hotspotY) -{ - wxImage image = p_bitmap.ConvertToImage(); - image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_X, p_hotspotX); - image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_Y, p_hotspotY); - return wxCursor(image); -} - -} // namespace - void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) { if (p_event.LeftIsDown() && p_event.Dragging()) { @@ -1057,38 +991,16 @@ void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) source.DoDragDrop(wxDrag_DefaultMove); return; } + node = m_layout.OutcomeHitTest(x, y); if (node && node->GetOutcome()) { - const wxBitmap bitmap = MakeOutcomeBitmap(); -#if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - - if (p_event.ControlDown()) { - wxString label; - label << "O" << node->GetNumber(); - wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(true); - } - else if (p_event.ShiftDown()) { - wxString label; - label << "p" << node->GetNumber(); - wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(true); - } - else { - wxString label; - label << "o" << node->GetNumber(); - wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(wxDrag_DefaultMove); - } + wxString label; + label << "O" << node->GetNumber(); + wxTextDataObject textData(label); + + wxDropSource source(textData, this); + source.DoDragDrop(wxDrag_DefaultMove); } } } diff --git a/src/gui/efgdisplay.h b/src/gui/efgdisplay.h index 7ef2d7fcd..b7df8c124 100644 --- a/src/gui/efgdisplay.h +++ b/src/gui/efgdisplay.h @@ -111,6 +111,8 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { bool ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, const wxPoint &p_pos); + bool ShowOutcomeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, + const wxPoint &p_pos); DECLARE_EVENT_TABLE() }; From 2184f83c2062e17ac55dbc803eddb4259760203e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 16:16:25 +0100 Subject: [PATCH 05/12] Remove DnD cursors for player drag as well --- src/gui/efgdisplay.cc | 1 - src/gui/efgpanel.cc | 12 +----------- src/gui/nfgpanel.cc | 12 +----------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index bc5a523fa..4ee455049 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -27,7 +27,6 @@ #include #endif // WX_PRECOMP #include // for drag-and-drop support -#include #include "gambit.h" diff --git a/src/gui/efgpanel.cc b/src/gui/efgpanel.cc index 4767fb47b..a58950f2e 100644 --- a/src/gui/efgpanel.cc +++ b/src/gui/efgpanel.cc @@ -27,7 +27,6 @@ #include #endif // WX_PRECOMP #include // for drag-and-drop features -#include // for creating drag-and-drop cursor #include // for printing support #include // for picking player colors #include // for SVG output @@ -66,19 +65,10 @@ gbtTreePlayerIcon::gbtTreePlayerIcon(wxWindow *p_parent, int p_player) void gbtTreePlayerIcon::OnLeftClick(wxMouseEvent &) { - const wxBitmap bitmap(person_xpm); - -#if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - wxString label; label << "P" << m_player; wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); + wxDropSource source(textData, this); source.DoDragDrop(wxDrag_DefaultMove); } diff --git a/src/gui/nfgpanel.cc b/src/gui/nfgpanel.cc index c72df8620..91de60321 100644 --- a/src/gui/nfgpanel.cc +++ b/src/gui/nfgpanel.cc @@ -25,7 +25,6 @@ #include #endif // WX_PRECOMP #include // for drag-and-drop features -#include // for creating drag-and-drop cursor #include // for picking player colors #include "gamedoc.h" @@ -62,19 +61,10 @@ TablePlayerIcon::TablePlayerIcon(wxWindow *p_parent, int p_player) void TablePlayerIcon::OnLeftClick(wxMouseEvent &) { - const wxBitmap bitmap(person_xpm); - -#if defined(__WXMSW__) or defined(__WXMAC__) - const auto image = wxCursor(bitmap.ConvertToImage()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - wxString label; label << "P" << m_player; wxTextDataObject textData(label); - wxDropSource source(textData, this, image, image, image); + wxDropSource source(textData, this); source.DoDragDrop(wxDrag_DefaultMove); } From f198dfc15ba7224ae16da1e58b83f5d9316f695a Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 16:22:23 +0100 Subject: [PATCH 06/12] Update drag-and-drop documentation --- doc/gui.efg.rst | 46 ++++++++++------------------------------------ 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/doc/gui.efg.rst b/doc/gui.efg.rst index a863bdb1e..6566580d0 100644 --- a/doc/gui.efg.rst +++ b/doc/gui.efg.rst @@ -84,27 +84,11 @@ the tree. It is often efficient to create the structure once, and then copy it as needed elsewhere. Gambit provides a convenient idiom for this. Clicking on any -nonterminal node and dragging to any terminal node implements a move -operation, which moves the entire subtree rooted at the original, -nonterminal node to the terminal node. - -To turn the operation into a copy operation: - -+ On Windows and Linux systems, hold down the :kbd:`Ctrl` key during - the operation. -+ On OS X, hold down the :kbd:`Cmd` key when starting the - drag operation, then release prior to dropping. - -The entire subtree rooted at the original node is copied, -starting at the terminal node. In this copy operation, each node in -the copied image is placed in the same information set as the -corresponding node in the original subtree. - -Copying a subtree to a terminal node in that subtree is also -supported. In this case, the copying operation is halted when reaching -the terminal node, to avoid an infinite loop. Thus, this feature -can also be helpful in constructing multiple-stage games. - +nonterminal node and dragging to another node results in a context-aware +popup menu. Depending on the destination node, this menu offers +the option of copying the subtree rooted at the original node, moving +it entirely, or placing the destination node in the same information set +as the source node. Removing parts of a game tree @@ -204,21 +188,11 @@ payoff by pressing the :kbd:`Tab` key both stores the changes to the player's payoff, and advances the editor to the payoff for the next player at that outcome. -Outcomes may also be moved or copied using a drag-and-drop idiom. -Left-clicking and dragging an outcome to another node moves the -outcome from the original node to the target node. Copying an outcome -may be accomplished by doing this same action while holding down the -Control (:kbd:`Ctrl`) key on the keyboard. - - - -When using the copy idiom described above, the action assigns the same -outcome to both the involved nodes. Therefore, if subsequently the -payoffs of the outcome are edited, the payoffs at both nodes will be -modified. To copy the outcome in such a way that the outcome at the -target node is a different outcome from the one at the source, but -with the same payoffs, hold down the :kbd:`Shift` key instead of the -:kbd:`Control` key while dragging. +Outcomes may also be moved or copied using drag-and-drop. +Left-clicking and dragging an outcome to another node pops up a +context-aware menu which allows the outcome to be moved or copied, or +to create a new outcome with the same payoffs as the original one at +the new node. To remove an outcome from a node, click on the node, and select :menuselection:`Edit --> Remove outcome`. From 7d23aca696aecc60333cb75a103b043e21f35b93 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 17 Jun 2026 10:10:57 +0100 Subject: [PATCH 07/12] Added context menu for dropping player --- src/gui/efgdisplay.cc | 70 ++++++++++++++++++++++++++++++++++--------- src/gui/efgdisplay.h | 2 ++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 4ee455049..e8f2b1cfc 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -88,7 +88,7 @@ class PlayerDropTarget : public wxTextDropTarget { EfgDisplay *m_owner; GameDocument *m_model; - bool OnDropPlayer(const GameNode &p_node, const wxString &p_text); + bool OnDropPlayer(const GameNode &p_node, const wxString &p_text, const wxPoint &p_pos); bool OnDropOutcome(const GameNode &p_node, const wxString &p_text, const wxPoint &p_pos); bool OnDropTreeNode(const GameNode &p_node, const wxString &p_text, const wxPoint &p_pos); @@ -123,22 +123,18 @@ static GameNode GetNode(const GameNode &p_node, int p_id) } } -bool PlayerDropTarget::OnDropPlayer(const GameNode &p_node, const wxString &p_text) +bool PlayerDropTarget::OnDropPlayer(const GameNode &p_node, const wxString &p_text, + const wxPoint &p_pos) { long pl; - p_text.Right(p_text.Length() - 1).ToLong(&pl); + if (!p_text.Right(p_text.Length() - 1).ToLong(&pl)) { + return false; + } + const Game efg = m_model->GetGame(); const GamePlayer player = ((pl == 0) ? efg->GetChance() : efg->GetPlayer(pl)); - if (p_node->IsTerminal()) { - m_model->DoInsertMove(p_node, player, 2); - } - else if (p_node->GetPlayer() == player) { - m_model->DoInsertAction(p_node); - } - else { - m_model->DoSetPlayer(p_node, player); - } - return true; + + return m_owner->ShowPlayerDropMenu(p_node, player, p_pos); } bool PlayerDropTarget::OnDropOutcome(const GameNode &p_node, const wxString &p_text, @@ -189,7 +185,7 @@ bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_te case 'N': return OnDropTreeNode(node, p_text, wxPoint(p_x, p_y)); case 'P': - return OnDropPlayer(node, p_text); + return OnDropPlayer(node, p_text, wxPoint(p_x, p_y)); case 'O': return OnDropOutcome(node, p_text, wxPoint(p_x, p_y)); default: @@ -258,6 +254,52 @@ void EfgDisplay::MakeMenus() m_nodeMenu->Append(GBT_MENU_EDIT_GAME, _("&Game properties"), _("Edit properties of the game")); } +bool EfgDisplay::ShowPlayerDropMenu(const GameNode &p_targetNode, const GamePlayer &p_player, + const wxPoint &p_pos) +{ + if (!p_targetNode || !p_player) { + return false; + } + + const int operationId = wxWindow::NewControlId(); + + wxMenu menu; + + if (p_targetNode->IsTerminal()) { + menu.Append(operationId, _("Insert move for this player")); + } + else if (p_targetNode->GetPlayer() == p_player) { + menu.Append(operationId, _("Insert action at this move")); + } + else { + menu.Append(operationId, _("Assign this move to this player")); + } + + const int selection = GetPopupMenuSelectionFromUser(menu, p_pos); + if (selection != operationId) { + return false; + } + + try { + if (p_targetNode->IsTerminal()) { + m_doc->DoInsertMove(p_targetNode, p_player, 2); + } + else if (p_targetNode->GetPlayer() == p_player) { + m_doc->DoInsertAction(p_targetNode); + } + else { + m_doc->DoSetPlayer(p_targetNode, p_player); + } + + return true; + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } + + return false; +} + bool EfgDisplay::ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, const wxPoint &p_pos) { diff --git a/src/gui/efgdisplay.h b/src/gui/efgdisplay.h index b7df8c124..268349e23 100644 --- a/src/gui/efgdisplay.h +++ b/src/gui/efgdisplay.h @@ -109,6 +109,8 @@ class EfgDisplay final : public wxScrolledWindow, public GameView { void EnsureNodeVisible(const GameNode &); + bool ShowPlayerDropMenu(const GameNode &p_targetNode, const GamePlayer &p_player, + const wxPoint &p_pos); bool ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, const wxPoint &p_pos); bool ShowOutcomeDropMenu(const GameNode &p_targetNode, const GameNode &p_sourceNode, From e54cbd56be68dbb35f4b86afd5b2c3fc924879aa Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 17 Jun 2026 10:14:57 +0100 Subject: [PATCH 08/12] Remove unneeded elses and nesting --- src/gui/efgdisplay.cc | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index e8f2b1cfc..3bcbd6a1d 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -110,17 +110,15 @@ static GameNode GetNode(const GameNode &p_node, int p_id) if (p_node->GetNumber() == p_id) { return p_node; } - else if (p_node->IsTerminal()) { + if (p_node->IsTerminal()) { return nullptr; } - else { - for (const auto &child : p_node->GetChildren()) { - if (const auto node = GetNode(child, p_id)) { - return node; - } + for (const auto &child : p_node->GetChildren()) { + if (const auto node = GetNode(child, p_id)) { + return node; } - return nullptr; } + return nullptr; } bool PlayerDropTarget::OnDropPlayer(const GameNode &p_node, const wxString &p_text, From c675f546a4ebc86c186723f3c8c44af089958ab8 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 17 Jun 2026 10:29:51 +0100 Subject: [PATCH 09/12] Add context menu for strategic form drag and drop --- src/gui/nfgtable.cc | 153 ++++++++++++++++++++++++++++++++------------ 1 file changed, 112 insertions(+), 41 deletions(-) diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index add5f6df4..193c2da50 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -113,7 +113,7 @@ void TableWidgetBase::OnCellLeftClick(wxSheetEvent &p_event) } //========================================================================= -// class gbtTableWidgetDropTarget +// class TableWidgetDropTarget //========================================================================= //! @@ -121,12 +121,11 @@ void TableWidgetBase::OnCellLeftClick(wxSheetEvent &p_event) //! communicates the location and text of the drop to its owner for //! further processing //! -class gbtTableWidgetDropTarget : public wxTextDropTarget { -private: +class TableWidgetDropTarget : public wxTextDropTarget { TableWidgetBase *m_owner; public: - explicit gbtTableWidgetDropTarget(TableWidgetBase *p_owner) : m_owner(p_owner) {} + explicit TableWidgetDropTarget(TableWidgetBase *p_owner) : m_owner(p_owner) {} bool OnDropText(wxCoord x, wxCoord y, const wxString &p_text) override { @@ -159,10 +158,13 @@ class RowPlayerWidget final : public TableWidgetBase { void OnCellRightClick(wxSheetEvent &); + bool ShowPlayerDropMenu(int p_index, int p_player, const wxString &p_label, + const wxPoint &p_pos); + public: /// @name Lifecycle //@{ - /// Constructorw + /// Constructor RowPlayerWidget(TableWidget *p_parent); //@} @@ -187,7 +189,7 @@ RowPlayerWidget::RowPlayerWidget(TableWidget *p_parent) SetScrollBarMode(SB_NEVER); SetGridLineColour(*wxBLACK); - wxWindow::SetDropTarget(new gbtTableWidgetDropTarget(this)); + wxWindow::SetDropTarget(new TableWidgetDropTarget(this)); Connect(GetId(), wxEVT_SHEET_CELL_RIGHT_DOWN, reinterpret_cast(wxStaticCastEvent( @@ -322,28 +324,61 @@ void RowPlayerWidget::OnUpdate() Refresh(); } +bool RowPlayerWidget::ShowPlayerDropMenu(int p_index, int p_player, const wxString &p_label, + const wxPoint &p_pos) +{ + const int placePlayerId = wxWindow::NewControlId(); + + wxMenu menu; + menu.Append(placePlayerId, p_label); + + const int selection = GetPopupMenuSelectionFromUser(menu, p_pos); + if (selection != placePlayerId) { + return false; + } + + try { + m_table->SetRowPlayer(p_index, p_player); + return true; + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } + + return false; +} + bool RowPlayerWidget::DropText(wxCoord p_x, wxCoord p_y, const wxString &p_text) { - if (p_text[0] == 'P') { - long pl; - p_text.Right(p_text.Length() - 1).ToLong(&pl); + if (p_text.empty() || p_text[0] != 'P') { + return false; + } - if (m_table->NumRowPlayers() == 0) { - m_table->SetRowPlayer(1, pl); - return true; - } + long player; + if (!p_text.Right(p_text.Length() - 1).ToLong(&player)) { + return false; + } - for (int col = 0; col < GetNumberCols(); col++) { - const wxRect rect = CellToRect(wxSheetCoords(0, col)); + if (m_table->NumRowPlayers() == 0) { + return ShowPlayerDropMenu(1, static_cast(player), _("Use as row player"), + wxPoint(p_x, p_y)); + } - if (p_x >= rect.x && p_x < rect.x + rect.width / 2) { - m_table->SetRowPlayer(col + 1, pl); - return true; - } - else if (p_x >= rect.x + rect.width / 2 && p_x < rect.x + rect.width) { - m_table->SetRowPlayer(col + 2, pl); - return true; - } + for (int col = 0; col < GetNumberCols(); col++) { + const wxRect rect = CellToRect(wxSheetCoords(0, col)); + const int existingPlayer = m_table->GetRowHeaderPlayer(col); + const wxString playerLabel = wxString::Format(_("Player %d"), existingPlayer); + + if (p_x >= rect.x && p_x < rect.x + rect.width / 2) { + return ShowPlayerDropMenu(col + 1, static_cast(player), + wxString::Format(_("Place before %s"), playerLabel), + wxPoint(p_x, p_y)); + } + + if (p_x >= rect.x + rect.width / 2 && p_x < rect.x + rect.width) { + return ShowPlayerDropMenu(col + 2, static_cast(player), + wxString::Format(_("Place after %s"), playerLabel), + wxPoint(p_x, p_y)); } } @@ -375,6 +410,9 @@ class ColPlayerWidget final : public TableWidgetBase { void OnCellRightClick(wxSheetEvent &); + bool ShowPlayerDropMenu(int p_index, int p_player, const wxString &p_label, + const wxPoint &p_pos); + public: /// @name Lifecycle //@{ @@ -404,7 +442,7 @@ ColPlayerWidget::ColPlayerWidget(TableWidget *p_parent) SetGridLineColour(*wxBLACK); wxWindow::SetBackgroundColour(*wxLIGHT_GREY); - wxWindow::SetDropTarget(new gbtTableWidgetDropTarget(this)); + wxWindow::SetDropTarget(new TableWidgetDropTarget(this)); Connect(GetId(), wxEVT_SHEET_CELL_RIGHT_DOWN, reinterpret_cast(wxStaticCastEvent( @@ -540,28 +578,61 @@ void ColPlayerWidget::DrawCell(wxDC &p_dc, const wxSheetCoords &p_coords) } } +bool ColPlayerWidget::ShowPlayerDropMenu(int p_index, int p_player, const wxString &p_label, + const wxPoint &p_pos) +{ + const int placePlayerId = wxWindow::NewControlId(); + + wxMenu menu; + menu.Append(placePlayerId, p_label); + + const int selection = GetPopupMenuSelectionFromUser(menu, p_pos); + if (selection != placePlayerId) { + return false; + } + + try { + m_table->SetColPlayer(p_index, p_player); + return true; + } + catch (std::exception &ex) { + ExceptionDialog(this, ex.what()).ShowModal(); + } + + return false; +} + bool ColPlayerWidget::DropText(wxCoord p_x, wxCoord p_y, const wxString &p_text) { - if (p_text[0] == 'P') { - long pl; - p_text.Right(p_text.Length() - 1).ToLong(&pl); + if (p_text.empty() || p_text[0] != 'P') { + return false; + } - if (m_table->NumColPlayers() == 0) { - m_table->SetColPlayer(1, pl); - return true; - } + long player; + if (!p_text.Right(p_text.Length() - 1).ToLong(&player)) { + return false; + } - for (int row = 0; row < GetNumberRows(); row++) { - const wxRect rect = CellToRect(wxSheetCoords(row, 0)); + if (m_table->NumColPlayers() == 0) { + return ShowPlayerDropMenu(1, static_cast(player), _("Use as column player"), + wxPoint(p_x, p_y)); + } - if (p_y >= rect.y && p_y < rect.y + rect.height / 2) { - m_table->SetColPlayer(row + 1, pl); - return true; - } - else if (p_y >= rect.y + rect.height / 2 && p_y < rect.y + rect.height) { - m_table->SetColPlayer(row + 2, pl); - return true; - } + for (int row = 0; row < GetNumberRows(); row++) { + const wxRect rect = CellToRect(wxSheetCoords(row, 0)); + const int existingPlayer = m_table->GetColHeaderPlayer(row); + const wxString playerLabel = wxString::Format(_("Player %d"), existingPlayer); + + if (p_y >= rect.y && p_y < rect.y + rect.height / 2) { + return ShowPlayerDropMenu(row + 1, static_cast(player), + wxString::Format(_("Place before %s"), playerLabel), + wxPoint(p_x, p_y)); + } + + if (p_y >= rect.y + rect.height / 2 && p_y < rect.y + rect.height) { + return ShowPlayerDropMenu(row + 2, static_cast(player), + wxString::Format(_("Place after %s"), playerLabel), + wxPoint(p_x, p_y)); } } From cf510d89d1a01c77dab4b58da6626bcd4bcb09b3 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 17 Jun 2026 10:36:57 +0100 Subject: [PATCH 10/12] Suppress popup if normal form DnD operation is a no-op --- src/gui/nfgtable.cc | 32 ++++++++++++++++++++++++++++++++ src/gui/nfgtable.h | 2 ++ 2 files changed, 34 insertions(+) diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index 193c2da50..b1beaa088 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -327,6 +327,10 @@ void RowPlayerWidget::OnUpdate() bool RowPlayerWidget::ShowPlayerDropMenu(int p_index, int p_player, const wxString &p_label, const wxPoint &p_pos) { + if (m_table->IsRowPlayerPlacementNoOp(p_index, p_player)) { + return false; + } + const int placePlayerId = wxWindow::NewControlId(); wxMenu menu; @@ -581,6 +585,10 @@ void ColPlayerWidget::DrawCell(wxDC &p_dc, const wxSheetCoords &p_coords) bool ColPlayerWidget::ShowPlayerDropMenu(int p_index, int p_player, const wxString &p_label, const wxPoint &p_pos) { + if (m_table->IsColPlayerPlacementNoOp(p_index, p_player)) { + return false; + } + const int placePlayerId = wxWindow::NewControlId(); wxMenu menu; @@ -1164,6 +1172,30 @@ bool TableWidget::ShowDominance() const { return m_nfgPanel->IsDominanceShown(); // TableWidget: View state //========================================================================= +bool TableWidget::IsRowPlayerPlacementNoOp(int p_index, int p_player) const +{ + for (int col = 0; col < NumRowPlayers(); ++col) { + if (GetRowHeaderPlayer(col) == p_player) { + const int currentIndex = col + 1; + return p_index == currentIndex || p_index == currentIndex + 1; + } + } + + return false; +} + +bool TableWidget::IsColPlayerPlacementNoOp(int p_index, int p_player) const +{ + for (int row = 0; row < NumColPlayers(); ++row) { + if (GetColHeaderPlayer(row) == p_player) { + const int currentIndex = row + 1; + return p_index == currentIndex || p_index == currentIndex + 1; + } + } + + return false; +} + void TableWidget::SetRowPlayer(int index, int pl) { m_layout->SetRowPlayer(index, pl); diff --git a/src/gui/nfgtable.h b/src/gui/nfgtable.h index 34bc459b8..8ceb1f92a 100644 --- a/src/gui/nfgtable.h +++ b/src/gui/nfgtable.h @@ -305,6 +305,8 @@ class TableWidget final : public wxPanel { /// Are we showing dominance indicators or not? bool ShowDominance() const; + bool IsRowPlayerPlacementNoOp(int p_index, int p_player) const; + bool IsColPlayerPlacementNoOp(int p_index, int p_player) const; //@} /// @name View state From f7b4c9b792b141f5afde2ac5f37203010cfd7e67 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 17 Jun 2026 11:25:23 +0100 Subject: [PATCH 11/12] Change no-op behaviour to silently accept rather than reject --- src/gui/nfgtable.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index b1beaa088..74df50984 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -328,7 +328,7 @@ bool RowPlayerWidget::ShowPlayerDropMenu(int p_index, int p_player, const wxStri const wxPoint &p_pos) { if (m_table->IsRowPlayerPlacementNoOp(p_index, p_player)) { - return false; + return true; } const int placePlayerId = wxWindow::NewControlId(); @@ -586,7 +586,7 @@ bool ColPlayerWidget::ShowPlayerDropMenu(int p_index, int p_player, const wxStri const wxPoint &p_pos) { if (m_table->IsColPlayerPlacementNoOp(p_index, p_player)) { - return false; + return true; } const int placePlayerId = wxWindow::NewControlId(); From adbf752219efcf822f6797ea9225ff4346673f69 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 18 Jun 2026 13:55:22 +0100 Subject: [PATCH 12/12] Rewrite documentation section on pivoting strategic game. --- doc/gui.nfg.rst | 181 +++++++++++++++++++++++++----------------------- 1 file changed, 96 insertions(+), 85 deletions(-) diff --git a/doc/gui.nfg.rst b/doc/gui.nfg.rst index af0f289c5..3631bd74c 100644 --- a/doc/gui.nfg.rst +++ b/doc/gui.nfg.rst @@ -22,91 +22,102 @@ game, select :menuselection:`File --> New --> Strategic game`, or click the new strategic game icon on the toolbar. - -Navigating a strategic game ---------------------------- - -Gambit displays a strategic game in table form. All players are -assigned to be either row players or column players, and the payoffs -for each entry in the strategic game table correspond to the payoffs -corresponding to the situation in which all the row players play the -strategy specified on that row for them, and all the column players -play the strategy specified on that column for them. - -.. image:: screens/pd1.* - :width: 33% - :alt: a prisoner's dilemma game - :align: right - :target: _images/pd1.png - -For games with two players, this presentation is by default configured -to be similar to the standard presenation of strategic games as -tables, in which one player is assigned to be the "row" player and the -other the "column" player. However, Gambit permits a more flexible -assignment, in which multiple players can be assigned to the rows and -multiple players to the columns. This is of particular use for games -with more than two players. In print, a three-player strategic game is -usually presented as a collection of tables, with one player choosing -the row, the second the column, and the third the table. Gambit -presents such games by hierarchially listing the strategies of one or -more players on both rows and columns. - -The hierarchical presentation of the table is similar to that of a -pivot table. -Here, Alice, -shown in red, has her strategies listed on the rows of the table, and -Bob, shown in blue, has his strategies listed on the columns of the -table. - -The assignment of players to row and column roles is fully -customizable. To change the assignment of a player, drag the person -icon appearing to the left of the player's name on the player toolbar -to either of the areas in the payoff table displaying the strategy -labels. - -.. image:: screens/pd2.* - :width: 33% - :alt: a prisoner's dilemma game, with contingencies in - list style - :align: right - :target: _images/pd2.png - -For example, dragging the player icon from the left of Bob's name in -the list of players and dropping it on the right side of Alice's -strategy label column changes the display of the game as in -Here, the strategies are shown in a -hierarchical format, enumerating the outcomes of the game first by -Alice's (red) strategy choice, then by Bob's (blue) strategy choice. - -Alternatively, the game can be displayed by listing the outcomes with -Bob's strategy choice first, then Alice's. Drag Bob's player icon and -drop it on the left side of Alice's strategy choices, and the game -display changes to organize the outcomes first by Bob's action, then -by Alice's. - -The same dragging operation can be used to assign players to the -columns. Assigning multiple players to the columns gives the same -hierarchical presentation of those players' strategies. Dropping a -player above another player's strategy labels assigns him to a higher -level of the column player hierarchy; dropping a player below another -player's strategy labels assigns him to a lower level of the column -player hierarchy. - -.. image:: screens/pd3.* - :width: 33% - :alt: another view of the same prisoner's dilemma game. - :align: right - :target: _images/pd3.png - -As the assignment of players in the row and column -hierarchies changes, the ordering of the payoffs in each cell of the -table also changes. In all cases, the color-coding of the entries -identifies the player to whom each payoff corresponds. The ordering -convention is chosen so that for a two player game in which one player -is a row player and the other a column player, the row player's payoff -is shown first, followed by the column player, which is the most -common convention in print. - +Displaying a strategic game +=========================== + +Gambit displays a strategic game as a table. Each cell represents one +strategy profile: one strategy choice for every player. The cell shows +the payoffs that result when the players choose the strategies named by +that cell's row and column labels. + +For a two-player game, Gambit initially uses the familiar matrix +arrangement. One player's strategies label the rows, and the other +player's strategies label the columns. + +.. image:: screens/pd1.* + :width: 50% + :alt: A two-player strategic game with Alice's strategies on the rows and Bob's strategies on the columns. + :align: center + :target: _images/pd1.png + +Each row-and-column combination identifies one strategy profile. For +example, a cell in the row labelled ``Cooperate`` and the column +labelled ``Defect`` represents the outcome in which the row player +chooses ``Cooperate`` and the column player chooses ``Defect``. + +The payoffs in each cell are colour-coded by player. In the standard +two-player arrangement, Gambit displays the row player's payoff first +and the column player's payoff second. + + +Row and column hierarchies +-------------------------- + +Games with more than two players require more than one strategy label +to identify each row or column. Gambit handles this by allowing several +players to be assigned to the rows, several players to be assigned to +the columns, or both. + +When several players are assigned to the same side of the table, their +strategy labels form a hierarchy. Each level groups together the +strategy combinations belonging to the players below it. This is +similar to the hierarchical row and column labels used in a pivot table +or a table with a multi-level index. + +For example, suppose Alice and Bob are both assigned to the rows. If +Alice is above Bob in the row hierarchy, Gambit first groups the rows +by Alice's strategy and then lists Bob's strategies within each group. + +.. image:: screens/pd2.* + :width: 50% + :alt: A strategic game with Alice and Bob arranged as two levels of hierarchical row labels. + :align: center + :target: _images/pd2.png + +Reversing their order groups the rows first by Bob's strategy and then +by Alice's. The strategy profiles and payoffs do not change; only their +arrangement in the table changes. + +The same principle applies to the columns. A player placed at a higher +level of the column hierarchy forms the outer grouping, while players +at lower levels form groups within it. + +This arrangement provides a single table view of games that are often +printed as a collection of separate payoff matrices. For example, a +three-player game can be displayed with one player on the rows and two +players in a column hierarchy, rather than as a separate matrix for +each strategy of the third player. + + +Rearranging the table +--------------------- + +To change the table arrangement, drag a player from the player list to +the row-label or column-label area. + +When the drop menu appears, choose where to place the player in the row +or column hierarchy. Gambit updates the display without changing the +game itself. + +The available positions depend on the current arrangement. Placing a +player before another player makes the moved player a higher level in +the hierarchy; placing the player after another player makes the moved +player a lower level. + +.. image:: screens/pd3.* + :width: 50% + :alt: The same strategic game displayed with a different ordering of its hierarchical strategy labels. + :align: center + :target: _images/pd3.png + +Changing the row and column hierarchies may also change the order in +which payoffs appear within each cell. The colour of each payoff +continues to identify the player to whom it belongs. + +.. note:: + + Rearranging the table changes only the presentation of the game. It + does not change the players, strategies, outcomes, or payoffs. Changing players and strategies