diff --git a/doc/gui.efg.rst b/doc/gui.efg.rst index c63c7cfdb..02488bd28 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 @@ -199,20 +183,10 @@ For nodes with existing outcomes, clicking on any of the displayed payoffs pops up an editing panel for that outcome. Outcomes may also be moved or copied using drag-and-drop. -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. +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`. 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 diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 01d7689f6..b22bc63ae 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -27,7 +27,6 @@ #include #endif // WX_PRECOMP #include -#include #include #include "gambit.h" @@ -349,13 +348,9 @@ class PlayerDropTarget : public wxTextDropTarget { EfgDisplay *m_owner; 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); + 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); public: explicit PlayerDropTarget(EfgDisplay *p_owner) @@ -375,121 +370,57 @@ 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) +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; -} -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; + return m_owner->ShowPlayerDropMenu(p_node, player, p_pos); } -bool PlayerDropTarget::OnDropMoveNode(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) { - 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) { + if (!srcNode || srcNode == p_node || !srcNode->GetOutcome()) { 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; - 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; + return m_owner->ShowOutcomeDropMenu(p_node, srcNode, p_pos); } -bool PlayerDropTarget::OnDropMoveOutcome(const GameNode &p_node, const wxString &p_text) +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 || p_node == srcNode) { - 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) { + if (!srcNode || srcNode == p_node || srcNode->IsTerminal()) { return false; } - m_model->DoCopyOutcome(p_node, srcNode->GetOutcome()); - return true; + + return m_owner->ShowTreeDropMenu(p_node, srcNode, p_pos); } bool PlayerDropTarget::OnDropText(wxCoord p_x, wxCoord p_y, const wxString &p_text) @@ -497,16 +428,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); @@ -518,20 +440,12 @@ 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': - return OnDropCopyNode(node, p_text); - case 'M': - return OnDropMoveNode(node, p_text); - case 'I': - return OnDropInfoset(node, p_text); + return OnDropPlayer(node, p_text, wxPoint(p_x, p_y)); 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; } @@ -595,6 +509,163 @@ 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) +{ + 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; +} + +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 //--------------------------------------------------------------------- @@ -1103,9 +1174,6 @@ void EfgDisplay::OnMagnify(wxMouseEvent &p_event) } } -#include "bitmaps/tree.xpm" -#include "bitmaps/move.xpm" - void EfgDisplay::OnMouseMotion(wxMouseEvent &p_event) { if (p_event.LeftIsDown() && p_event.Dragging()) { @@ -1117,93 +1185,24 @@ 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); -#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()); -#else - wxIcon image; - image.CopyFromBitmap(bitmap); -#endif // _WXMSW__ - - wxString label; - label << "M" << node->GetNumber(); - wxTextDataObject textData(label); - - wxDropSource source(textData, this, image, image, image); - /*wxDragResult result =*/source.DoDragDrop(wxDrag_DefaultMove); - } + wxString label; + label << "N" << node->GetNumber(); + wxTextDataObject textData(label); + + wxDropSource source(textData, this); + 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 899cf26c2..897ad2617 100644 --- a/src/gui/efgdisplay.h +++ b/src/gui/efgdisplay.h @@ -85,6 +85,13 @@ 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, + const wxPoint &p_pos); + DECLARE_EVENT_TABLE() }; } // namespace Gambit::GUI 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); } diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index add5f6df4..74df50984 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,65 @@ void RowPlayerWidget::OnUpdate() Refresh(); } +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 true; + } + + 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 +414,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 +446,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 +582,65 @@ 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 true; + } + + 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)); } } @@ -1093,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