From 9a7f1bb7e19cb7efa227b987b7a44d6db027f00a Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 5 Jun 2026 15:24:45 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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`.