mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Tests] Play card interactions through a Cockatrice event player (#7697)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
34201122ec
commit
3fa8d78786
14 changed files with 1119 additions and 3 deletions
|
|
@ -50,3 +50,17 @@ A test is a `<events>` list. Widgets are named by their path from the main windo
|
||||||
| `activate` | action or menu name | triggers a QAction (menus are opened first) |
|
| `activate` | action or menu name | triggers a QAction (menus are opened first) |
|
||||||
| `key` | Qt key code | presses that key on the focused widget |
|
| `key` | Qt key code | presses that key on the focused widget |
|
||||||
| `pause` | milliseconds | waits, letting the UI settle |
|
| `pause` | milliseconds | waits, letting the UI settle |
|
||||||
|
| `clickCard` | `card,zone` | double click the card found in that zone (plays it from hand) |
|
||||||
|
| `flipCard` | `card,zone` | right-click → *Turn Over* |
|
||||||
|
| `cardAction` | `card,zone,label` | right-click → first menu entry whose label contains `label` (submenus included) |
|
||||||
|
| `moveCard` | `card,srcZone,dstZone` | drags the card onto the destination zone |
|
||||||
|
| `drawArrow` | `srcCard,srcZone,dstCard,dstZone` | right-drag an arrow between two cards |
|
||||||
|
| `createToken` | `name,color,pt` | creates a token as the active local player |
|
||||||
|
| `selectItem` | `item label` | selects the tree/list entry with that label |
|
||||||
|
| `typeText` | `text` | types text one key event at a time |
|
||||||
|
| `expectProperty` | `property,value` | fails playback unless the widget property matches |
|
||||||
|
|
||||||
|
The card, item and property commands are `pqCockatriceEventPlayer`
|
||||||
|
(`cockatrice/src/game_graphics/pq_cockatrice_event_player.h`). They resolve cards by name
|
||||||
|
and zone through the `QGraphicsScene` and send real mouse events at the card's current
|
||||||
|
viewport position, so a test does not depend on the window size or screen resolution.
|
||||||
|
|
|
||||||
69
Testing/card_actions_test.xml
Normal file
69
Testing/card_actions_test.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<events>
|
||||||
|
<!--
|
||||||
|
Card actions (custom commands of pqCockatriceEventPlayer).
|
||||||
|
|
||||||
|
Exercises the context menu path (tap, counters in a submenu, flip),
|
||||||
|
arrow drawing, and dragging a card between zones. All of it through
|
||||||
|
mouse events on the game view, so the same file runs at any resolution.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- ===== Two-player local game ===== -->
|
||||||
|
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="Start &local game..." />
|
||||||
|
<event widget="MainWindow/localGameDialog/generalGroupBox/numberPlayersEdit" command="set_int" arguments="2" />
|
||||||
|
<event widget="MainWindow/localGameDialog" command="key" arguments="16777220" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
|
||||||
|
<!-- ===== Player 1 loads a one-card deck ===== -->
|
||||||
|
<!-- (No tab-count assertion: the restored workspace state decides how many other
|
||||||
|
tabs exist, and the paths below already prove the game tab is there.) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/loadFromClipboardButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="typeText" arguments="1 Lightning Bolt" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit" command="keyEvent" arguments="6,16777220,67108864,,0,1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/readyStartButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
|
||||||
|
<!-- ===== Player 2 loads the same deck and readies up ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/loadFromClipboardButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="typeText" arguments="1 Lightning Bolt" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit" command="keyEvent" arguments="6,16777220,67108864,,0,1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/readyStartButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
|
||||||
|
<!-- ===== Player 1, precombat main ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- ===== A token to interact with ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="createToken" arguments="Goblin,r,1/1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
|
||||||
|
<!-- Tap it through the context menu -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="cardAction" arguments="Goblin,table,Tap / Untap" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
|
||||||
|
<!-- Add a counter: the entry lives in the "Card counters" submenu -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="cardAction" arguments="Goblin,table,Add counter (A)" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
|
||||||
|
<!-- Flip it face down -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="flipCard" arguments="Goblin,table" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
|
||||||
|
<!-- ===== An arrow from a second token to the Goblin ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="createToken" arguments="Elf,w,1/1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="drawArrow" arguments="Elf,table,Goblin,table" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- ===== Drag the Goblin into the graveyard ===== -->
|
||||||
|
<!-- A token that leaves the battlefield is destroyed rather than moved, so
|
||||||
|
there is nothing to drag back afterwards. -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="moveCard" arguments="Goblin,table,grave" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
</events>
|
||||||
101
Testing/game_mechanics_test.xml
Normal file
101
Testing/game_mechanics_test.xml
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<events>
|
||||||
|
<!--
|
||||||
|
Tier 7: Game Mechanics
|
||||||
|
Tests: play cards (clickCard), flip cards (flipCard),
|
||||||
|
create tokens (createToken), draw arrows (drawArrow).
|
||||||
|
All interactions are pure UI mouse events through the QGraphicsView viewport.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- ===== Start a local 2-player game ===== -->
|
||||||
|
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="Start &local game..." />
|
||||||
|
<!-- Force two players so both deck views exist, independent of the user's settings -->
|
||||||
|
<event widget="MainWindow/localGameDialog/generalGroupBox/numberPlayersEdit" command="set_int" arguments="2" />
|
||||||
|
<event widget="MainWindow/localGameDialog" command="key" arguments="16777220" />
|
||||||
|
|
||||||
|
<!-- ===== Player 1: Load 3-card deck ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/loadFromClipboardButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="keyEvent" arguments="6,65,67108864,,0,1" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="typeText" arguments="3 Lightning Bolt" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit" command="keyEvent" arguments="6,16777220,67108864,,0,1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/readyStartButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
|
||||||
|
<!-- ===== Player 2: Load same deck ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/loadFromClipboardButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="keyEvent" arguments="6,65,67108864,,0,1" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="typeText" arguments="3 Lightning Bolt" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit" command="keyEvent" arguments="6,16777220,67108864,,0,1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/readyStartButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
|
||||||
|
<!-- ===== Player 1, Turn 1 ===== -->
|
||||||
|
<!-- Upkeep → Draw → Main 1 -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- Create a Goblin token -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="createToken" arguments="Goblin,r,1/1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
|
||||||
|
<!-- Create a second token (Elf) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="createToken" arguments="Elf,w,1/1" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="1000" />
|
||||||
|
|
||||||
|
<!-- Flip the Goblin (right-click → Turn Over) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="flipCard" arguments="Goblin,table" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- Play a Lightning Bolt from hand (double-click) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="clickCard" arguments="Lightning Bolt,hand" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" -->
|
||||||
|
|
||||||
|
<!-- Draw arrow from Elf to Goblin (right-drag) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="drawArrow" arguments="Elf,table,Goblin,table" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- Advance through all remaining phases to end turn -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- ===== Player 2, Turn 1 ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<!-- Advance through all phases to end turn -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="200" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
</events>
|
||||||
178
Testing/local_game_lifecycle_test.xml
Normal file
178
Testing/local_game_lifecycle_test.xml
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
<events>
|
||||||
|
<!--
|
||||||
|
Tier 6: Local Game Lifecycle (Extended)
|
||||||
|
Tests: create game -> load decks -> ready up -> game starts
|
||||||
|
-> play through turns using menu activate commands
|
||||||
|
|
||||||
|
Phase actions triggered via activate on phasesMenu:
|
||||||
|
aNextPhaseAction advances phase + triggers phase action (e.g., draw at draw step)
|
||||||
|
aNextTurn in gameMenu triggers next turn
|
||||||
|
|
||||||
|
This avoids keyboard shortcut issues in offscreen/test environments.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- ===== Phase 1: Start a local game via main menu ===== -->
|
||||||
|
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="Start &local game..." />
|
||||||
|
<!-- Force two players so both deck views exist, independent of the user's settings -->
|
||||||
|
<event widget="MainWindow/localGameDialog/generalGroupBox/numberPlayersEdit" command="set_int" arguments="2" />
|
||||||
|
<event widget="MainWindow/localGameDialog" command="key" arguments="16777220" />
|
||||||
|
|
||||||
|
<!-- ===== Phase 2: Player 1 loads deck from clipboard ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/loadFromClipboardButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="keyEvent" arguments="6,65,67108864,,0,1" />
|
||||||
|
<!-- 1 Lightning Bolt -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="typeText" arguments="1 Lightning Bolt" />
|
||||||
|
<!-- Ctrl+Return to accept clipboard dialog -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit" command="keyEvent" arguments="6,16777220,67108864,,0,1" />
|
||||||
|
<!-- Delay -->
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Player 1 ready up -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_0/qt_tabwidget_stackedwidget/deckViewContainer/readyStartButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
|
||||||
|
<!-- ===== Phase 3: Player 2 loads deck from clipboard ===== -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/loadFromClipboardButton" command="activate" arguments="" />
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="keyEvent" arguments="6,65,67108864,,0,1" />
|
||||||
|
<!-- 1 Lightning Bolt -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit/contentsEdit" command="typeText" arguments="1 Lightning Bolt" />
|
||||||
|
<!-- Ctrl+Return to accept clipboard dialog -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/dlgDeckTextEdit" command="keyEvent" arguments="6,16777220,67108864,,0,1" />
|
||||||
|
<!-- Delay -->
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Player 2 ready up -> game auto-starts -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/deckViewContainerWidget/tabbedDeckView_1/qt_tabwidget_stackedwidget/deckViewContainer/readyStartButton" command="activate" arguments="" />
|
||||||
|
<!-- Delay for game to start -->
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
|
||||||
|
<!-- ===== Phase 4: Advance through all 11 phases via phasesMenu activate ===== -->
|
||||||
|
<!-- Using activate on phasesMenu triggers aNextPhaseAction which:
|
||||||
|
- advances the phase by 1
|
||||||
|
- calls triggerPhaseAction (e.g., draw card at draw step, untap all at untap) -->
|
||||||
|
|
||||||
|
<!-- Phase 0 (Untap) → 1 (Upkeep) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 1 (Upkeep) → 2 (Draw): TRIGGERS DRAW — Player 1 draws 1 card -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
<!-- Phase 2 (Draw) → 3 (Main 1) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- ===== Main 1: Play a card from hand by double-click ===== -->
|
||||||
|
<!-- clickCard resolves card by name in the scene, maps to viewport coords at current window size. -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/mainWidget/gamePlayAreaWidget/gameView" command="clickCard" arguments="Lightning Bolt,hand" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="500" />
|
||||||
|
|
||||||
|
<!-- Phase 3 → 4 (Begin Combat) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="pause" arguments="300" />
|
||||||
|
<!-- Phase 4 → 5 (Declare Attackers) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 5 → 6 (Declare Blockers) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 6 → 7 (Combat Damage) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 7 → 8 (End Combat) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 8 → 9 (Main 2) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 9 → 10 (Cleanup) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 10 → 0 (Untap): TRIGGERS NEXT TURN + UNTAP ALL — now Player 2's turn -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<!-- Delay for turn transition -->
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
|
||||||
|
<!-- ===== Phase 5: Player 2's turn — advance through all 11 phases ===== -->
|
||||||
|
<!-- Phase 0 (Untap) → 1 (Upkeep) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 1 (Upkeep) → 2 (Draw): TRIGGERS DRAW — Player 2 draws 1 card -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 2 (Draw) → 3 (Main 1) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 3 → 4 (Begin Combat) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 4 → 5 (Declare Attackers) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 5 → 6 (Declare Blockers) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 6 → 7 (Combat Damage) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 7 → 8 (End Combat) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 8 → 9 (Main 2) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 9 → 10 (Cleanup) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 10 → 0 (Untap): TRIGGERS NEXT TURN — back to Player 1 -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<!-- Delay -->
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
|
||||||
|
<!-- ===== Phase 6: Player 1 turn 2 — advance to draw again ===== -->
|
||||||
|
<!-- Phase 0 (Untap) → 1 (Upkeep) -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<!-- Phase 1 (Upkeep) → 2 (Draw): TRIGGERS DRAW — Player 1 draws again -->
|
||||||
|
<event widget="MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu" command="activate" arguments="aNextPhaseAction" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
<event widget="MainWindow" command="key" arguments="32" />
|
||||||
|
|
||||||
|
</events>
|
||||||
|
|
@ -567,9 +567,9 @@ target_link_libraries(
|
||||||
cockatrice_target_enable_crash_reporting(cockatrice)
|
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||||
|
|
||||||
if(WITH_GUI_TEST)
|
if(WITH_GUI_TEST)
|
||||||
# recording.cpp translates between XML test files and the QtTesting event pipeline;
|
# These two translate between XML test files and the QtTesting event pipeline;
|
||||||
# it needs the QtTesting headers, so it only exists with the harness.
|
# they need the QtTesting headers, so they only exist with the harness.
|
||||||
target_sources(cockatrice PRIVATE src/interface/recording.cpp)
|
target_sources(cockatrice PRIVATE src/game_graphics/pq_cockatrice_event_player.cpp src/interface/recording.cpp)
|
||||||
target_link_libraries(cockatrice PRIVATE QtTesting)
|
target_link_libraries(cockatrice PRIVATE QtTesting)
|
||||||
target_compile_definitions(cockatrice PRIVATE WITH_GUI_TEST)
|
target_compile_definitions(cockatrice PRIVATE WITH_GUI_TEST)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ void ToggleButton::setState(bool _state)
|
||||||
DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
: QWidget(nullptr), visualDeckStorageWidget(nullptr), parentGame(parent), playerId(_playerId)
|
: QWidget(nullptr), visualDeckStorageWidget(nullptr), parentGame(parent), playerId(_playerId)
|
||||||
{
|
{
|
||||||
|
setObjectName("deckViewContainer");
|
||||||
|
|
||||||
loadLocalButton = new QPushButton;
|
loadLocalButton = new QPushButton;
|
||||||
loadRemoteButton = new QPushButton;
|
loadRemoteButton = new QPushButton;
|
||||||
loadFromClipboardButton = new QPushButton;
|
loadFromClipboardButton = new QPushButton;
|
||||||
|
|
@ -80,6 +82,14 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
forceStartGameButton = new QPushButton;
|
forceStartGameButton = new QPushButton;
|
||||||
sideboardLockButton = new ToggleButton;
|
sideboardLockButton = new ToggleButton;
|
||||||
|
|
||||||
|
loadLocalButton->setObjectName("loadLocalButton");
|
||||||
|
loadRemoteButton->setObjectName("loadRemoteButton");
|
||||||
|
loadFromClipboardButton->setObjectName("loadFromClipboardButton");
|
||||||
|
readyStartButton->setObjectName("readyStartButton");
|
||||||
|
forceStartGameButton->setObjectName("forceStartGameButton");
|
||||||
|
unloadDeckButton->setObjectName("unloadDeckButton");
|
||||||
|
sideboardLockButton->setObjectName("sideboardLockButton");
|
||||||
|
|
||||||
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
|
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
|
||||||
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
|
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
|
||||||
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
|
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class TabGame;
|
||||||
TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent)
|
TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent)
|
||||||
: QTabWidget(nullptr), playerId(_playerId), parentGame(parent)
|
: QTabWidget(nullptr), playerId(_playerId), parentGame(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName(QString("tabbedDeckView_%1").arg(_playerId));
|
||||||
setTabsClosable(true);
|
setTabsClosable(true);
|
||||||
connect(this, &QTabWidget::tabCloseRequested, this, &TabbedDeckViewContainer::closeTab);
|
connect(this, &QTabWidget::tabCloseRequested, this, &TabbedDeckViewContainer::closeTab);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ protected:
|
||||||
|
|
||||||
GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0)
|
GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0)
|
||||||
{
|
{
|
||||||
|
setObjectName("gameView");
|
||||||
setBackgroundBrush(QBrush(QColor(0, 0, 0)));
|
setBackgroundBrush(QBrush(QColor(0, 0, 0)));
|
||||||
setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing);
|
setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing);
|
||||||
setViewportUpdateMode(BoundingRectViewportUpdate);
|
setViewportUpdateMode(BoundingRectViewportUpdate);
|
||||||
|
|
|
||||||
628
cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp
Normal file
628
cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp
Normal file
|
|
@ -0,0 +1,628 @@
|
||||||
|
#include "pq_cockatrice_event_player.h"
|
||||||
|
|
||||||
|
#include "../../interface/widgets/tabs/tab_game.h"
|
||||||
|
#include "board/card_item.h"
|
||||||
|
#include "game_scene.h"
|
||||||
|
#include "game_view.h"
|
||||||
|
#include "zones/card_zone.h"
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QGraphicsScene>
|
||||||
|
#include <QGraphicsView>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <libcockatrice/game/abstract_game.h>
|
||||||
|
#include <libcockatrice/game/game_state.h>
|
||||||
|
#include <libcockatrice/game/player/player_actions.h>
|
||||||
|
#include <libcockatrice/game/player/player_logic.h>
|
||||||
|
#include <libcockatrice/game/player/player_manager.h>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(PQ_COCKATRICE_EVENT_PLAYER, "cockatrice.guiTest.eventPlayer")
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Strips Qt's mnemonic markers so a test can name a menu entry by the label the
|
||||||
|
* user sees ("Turn Over" rather than "T&urn Over").
|
||||||
|
*/
|
||||||
|
QString normalizeActionText(const QString &text)
|
||||||
|
{
|
||||||
|
QString normalized = text;
|
||||||
|
normalized.remove('&');
|
||||||
|
return normalized.simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a printable character onto the key a US keyboard would report for it, so
|
||||||
|
* typing produces the same QKeyEvents a recording would.
|
||||||
|
*/
|
||||||
|
Qt::Key keyForCharacter(QChar character)
|
||||||
|
{
|
||||||
|
if (character.isLetter()) {
|
||||||
|
return static_cast<Qt::Key>(Qt::Key_A + (character.toUpper().unicode() - 'A'));
|
||||||
|
}
|
||||||
|
if (character.isDigit()) {
|
||||||
|
return static_cast<Qt::Key>(Qt::Key_0 + (character.unicode() - '0'));
|
||||||
|
}
|
||||||
|
switch (character.unicode()) {
|
||||||
|
case ' ':
|
||||||
|
return Qt::Key_Space;
|
||||||
|
case '\n':
|
||||||
|
return Qt::Key_Return;
|
||||||
|
case '\t':
|
||||||
|
return Qt::Key_Tab;
|
||||||
|
case '-':
|
||||||
|
return Qt::Key_Minus;
|
||||||
|
case '_':
|
||||||
|
return Qt::Key_Underscore;
|
||||||
|
case '/':
|
||||||
|
return Qt::Key_Slash;
|
||||||
|
case ',':
|
||||||
|
return Qt::Key_Comma;
|
||||||
|
case '.':
|
||||||
|
return Qt::Key_Period;
|
||||||
|
case '\'':
|
||||||
|
return Qt::Key_Apostrophe;
|
||||||
|
case ';':
|
||||||
|
return Qt::Key_Semicolon;
|
||||||
|
case ':':
|
||||||
|
return Qt::Key_Colon;
|
||||||
|
case '"':
|
||||||
|
return Qt::Key_QuoteDbl;
|
||||||
|
case '&':
|
||||||
|
return Qt::Key_Ampersand;
|
||||||
|
case '(':
|
||||||
|
return Qt::Key_ParenLeft;
|
||||||
|
case ')':
|
||||||
|
return Qt::Key_ParenRight;
|
||||||
|
case '[':
|
||||||
|
return Qt::Key_BracketLeft;
|
||||||
|
case ']':
|
||||||
|
return Qt::Key_BracketRight;
|
||||||
|
default:
|
||||||
|
return Qt::Key_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Depth-first search for the first model index whose label matches @p text. */
|
||||||
|
QModelIndex findModelIndexByText(const QAbstractItemModel *model, const QString &text, const QModelIndex &parent = {})
|
||||||
|
{
|
||||||
|
for (int row = 0; row < model->rowCount(parent); ++row) {
|
||||||
|
const QModelIndex index = model->index(row, 0, parent);
|
||||||
|
if (index.data(Qt::DisplayRole).toString() == text) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
const QModelIndex child = findModelIndexByText(model, text, index);
|
||||||
|
if (child.isValid()) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needsShift(QChar character)
|
||||||
|
{
|
||||||
|
if (character.isUpper()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return QStringLiteral("_:\"&()").contains(character);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
pqCockatriceEventPlayer::pqCockatriceEventPlayer(QObject *parent) : pqWidgetEventPlayer(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playEvent(QObject *object, const QString &command, const QString &arguments, bool &error)
|
||||||
|
{
|
||||||
|
// A known command that fails is a handled-but-failed event: QtTesting logs
|
||||||
|
// "Unhandled event" when playEvent returns false, so report through `error`
|
||||||
|
// (the contract playExpectProperty already follows) and still return true.
|
||||||
|
const auto run = [&error](bool ok) {
|
||||||
|
if (!ok) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (command == "clickCard") {
|
||||||
|
return run(playClickCard(object, arguments));
|
||||||
|
} else if (command == "flipCard") {
|
||||||
|
return run(playFlipCard(object, arguments));
|
||||||
|
} else if (command == "cardAction") {
|
||||||
|
return run(playCardAction(object, arguments));
|
||||||
|
} else if (command == "moveCard") {
|
||||||
|
return run(playMoveCard(object, arguments));
|
||||||
|
} else if (command == "drawArrow") {
|
||||||
|
return run(playDrawArrow(object, arguments));
|
||||||
|
} else if (command == "createToken") {
|
||||||
|
return run(playCreateToken(object, arguments));
|
||||||
|
} else if (command == "selectItem") {
|
||||||
|
return run(playSelectItem(object, arguments));
|
||||||
|
} else if (command == "typeText") {
|
||||||
|
return run(playTypeText(object, arguments));
|
||||||
|
} else if (command == "expectProperty") {
|
||||||
|
return playExpectProperty(object, arguments, error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsItem *
|
||||||
|
pqCockatriceEventPlayer::findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName)
|
||||||
|
{
|
||||||
|
for (QGraphicsItem *gi : scene->items()) {
|
||||||
|
if (gi->type() != typeCard) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto *card = static_cast<CardItem *>(gi);
|
||||||
|
if (!card->getZone() || card->getZone()->getName() != zoneName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString actualName = card->getName();
|
||||||
|
if (actualName == cardName || actualName.endsWith(" " + cardName)) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsItem *pqCockatriceEventPlayer::findZoneInScene(QGraphicsScene *scene, const QString &zoneName)
|
||||||
|
{
|
||||||
|
for (QGraphicsItem *gi : scene->items()) {
|
||||||
|
if (gi->type() != typeZone) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto *zone = static_cast<CardZone *>(gi);
|
||||||
|
if (zone->getLogic() && zone->getLogic()->getName() == zoneName) {
|
||||||
|
return zone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAction *pqCockatriceEventPlayer::findActionInMenu(QMenu *menu, const QString &actionText)
|
||||||
|
{
|
||||||
|
if (!menu || actionText.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (QAction *action : menu->actions()) {
|
||||||
|
if (action->isSeparator()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalizeActionText(action->text()).contains(actionText, Qt::CaseInsensitive)) {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
if (QMenu *subMenu = action->menu()) {
|
||||||
|
if (QAction *found = findActionInMenu(subMenu, actionText)) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu *pqCockatriceEventPlayer::visiblePopupMenu()
|
||||||
|
{
|
||||||
|
for (QWidget *w : QApplication::topLevelWidgets()) {
|
||||||
|
auto *menu = qobject_cast<QMenu *>(w);
|
||||||
|
if (menu && menu->isVisible()) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint pqCockatriceEventPlayer::viewportPoint(QGraphicsView *view, const QGraphicsItem *item)
|
||||||
|
{
|
||||||
|
return view->mapFromScene(item->sceneBoundingRect().center());
|
||||||
|
}
|
||||||
|
void pqCockatriceEventPlayer::sendMouseEvent(QGraphicsView *view,
|
||||||
|
QEvent::Type type,
|
||||||
|
const QPoint &pos,
|
||||||
|
Qt::MouseButton button,
|
||||||
|
Qt::MouseButtons buttons)
|
||||||
|
{
|
||||||
|
QMouseEvent event(type, pos, view->viewport()->mapToGlobal(pos), button, buttons, Qt::NoModifier);
|
||||||
|
qApp->notify(view->viewport(), &event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playClickCard(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *gv = qobject_cast<QGraphicsView *>(object);
|
||||||
|
if (!gv) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: target widget is not a QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 2) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: arguments must be 'cardName,zoneName', got:" << arguments;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsScene *scene = gv->scene();
|
||||||
|
if (!scene) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: no scene on the QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *target = findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed());
|
||||||
|
if (!target) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "clickCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint pos = viewportPoint(gv, target);
|
||||||
|
// A double click is the play gesture from hand: press/release, then the
|
||||||
|
// double-click event and its release, exactly as a recording produces it.
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonPress, pos, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonDblClick, pos, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playFlipCard(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *gv = qobject_cast<QGraphicsView *>(object);
|
||||||
|
if (!gv) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: target widget is not a QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 2) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: arguments must be 'cardName,zoneName', got:" << arguments;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsScene *scene = gv->scene();
|
||||||
|
if (!scene) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: no scene";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *card = qgraphicsitem_cast<CardItem *>(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()));
|
||||||
|
if (!card) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "flipCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return invokeCardMenuAction("flipCard", gv, card, QStringLiteral("Turn Over"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playCardAction(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *gv = qobject_cast<QGraphicsView *>(object);
|
||||||
|
if (!gv) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: target widget is not a QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 3) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "cardAction: arguments must be 'cardName,zoneName,actionText', got:" << arguments;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsScene *scene = gv->scene();
|
||||||
|
if (!scene) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: no scene";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *card = qgraphicsitem_cast<CardItem *>(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()));
|
||||||
|
if (!card) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "cardAction: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return invokeCardMenuAction("cardAction", gv, card, parts.mid(2).join(',').trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::invokeCardMenuAction(const char *command,
|
||||||
|
QGraphicsView *view,
|
||||||
|
CardItem *card,
|
||||||
|
const QString &actionText)
|
||||||
|
{
|
||||||
|
const QPoint pos = viewportPoint(view, card);
|
||||||
|
sendMouseEvent(view, QEvent::MouseButtonPress, pos, Qt::RightButton, Qt::RightButton);
|
||||||
|
sendMouseEvent(view, QEvent::MouseButtonRelease, pos, Qt::RightButton, Qt::NoButton);
|
||||||
|
qApp->processEvents();
|
||||||
|
|
||||||
|
QMenu *menu = visiblePopupMenu();
|
||||||
|
if (!menu) {
|
||||||
|
qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no popup menu appeared after right-click";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAction *action = findActionInMenu(menu, actionText);
|
||||||
|
if (!action) {
|
||||||
|
qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no context menu action matching" << actionText;
|
||||||
|
menu->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
action->trigger();
|
||||||
|
menu->close();
|
||||||
|
qApp->processEvents();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playMoveCard(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *gv = qobject_cast<QGraphicsView *>(object);
|
||||||
|
if (!gv) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: target widget is not a QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 3) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "moveCard: arguments must be 'cardName,srcZoneName,dstZoneName', got:" << arguments;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString cardName = parts[0].trimmed();
|
||||||
|
const QString srcZoneName = parts[1].trimmed();
|
||||||
|
const QString dstZoneName = parts[2].trimmed();
|
||||||
|
|
||||||
|
QGraphicsScene *scene = gv->scene();
|
||||||
|
if (!scene) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: no scene";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *card = findCardInScene(scene, cardName, srcZoneName);
|
||||||
|
if (!card) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: card" << cardName << "not found in zone" << srcZoneName;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *dstZone = findZoneInScene(scene, dstZoneName);
|
||||||
|
if (!dstZone) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: destination zone" << dstZoneName << "not found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint srcPos = viewportPoint(gv, card);
|
||||||
|
const QPoint dstPos = viewportPoint(gv, dstZone);
|
||||||
|
|
||||||
|
// A drag has to travel past Qt's start distance before the scene turns it
|
||||||
|
// into a card drag item, otherwise the move is interpreted as a click.
|
||||||
|
const int dragDistance = 2 * QApplication::startDragDistance() + 10;
|
||||||
|
const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0);
|
||||||
|
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
qApp->processEvents();
|
||||||
|
sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::LeftButton, Qt::LeftButton);
|
||||||
|
qApp->processEvents();
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::LeftButton, Qt::NoButton);
|
||||||
|
qApp->processEvents();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playDrawArrow(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *gv = qobject_cast<QGraphicsView *>(object);
|
||||||
|
if (!gv) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: target widget is not a QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 4) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "drawArrow: arguments must be 'srcCard,srcZone,dstCard,dstZone', got:" << arguments;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString srcCardName = parts[0].trimmed();
|
||||||
|
const QString srcZoneName = parts[1].trimmed();
|
||||||
|
const QString dstCardName = parts[2].trimmed();
|
||||||
|
const QString dstZoneName = parts[3].trimmed();
|
||||||
|
|
||||||
|
QGraphicsScene *scene = gv->scene();
|
||||||
|
if (!scene) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: no scene";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *srcCard = findCardInScene(scene, srcCardName, srcZoneName);
|
||||||
|
if (!srcCard) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "drawArrow: source card" << srcCardName << "not found in zone" << srcZoneName;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *dstCard = findCardInScene(scene, dstCardName, dstZoneName);
|
||||||
|
if (!dstCard) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "drawArrow: dest card" << dstCardName << "not found in zone" << dstZoneName;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint srcPos = viewportPoint(gv, srcCard);
|
||||||
|
const QPoint dstPos = viewportPoint(gv, dstCard);
|
||||||
|
|
||||||
|
const int dragDistance = 2 * QApplication::startDragDistance() + 10;
|
||||||
|
const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0);
|
||||||
|
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::RightButton, Qt::RightButton);
|
||||||
|
sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::RightButton, Qt::RightButton);
|
||||||
|
qApp->processEvents();
|
||||||
|
sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::RightButton, Qt::RightButton);
|
||||||
|
sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::RightButton, Qt::NoButton);
|
||||||
|
qApp->processEvents();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playCreateToken(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *gv = qobject_cast<QGraphicsView *>(object);
|
||||||
|
if (!gv) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: target widget is not a QGraphicsView";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 3) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: arguments must be 'name,color,pt', got:" << arguments;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString tokenName = parts[0].trimmed();
|
||||||
|
QString tokenColor = parts[1].trimmed();
|
||||||
|
QString tokenPt = parts[2].trimmed();
|
||||||
|
|
||||||
|
TabGame *tabGame = nullptr;
|
||||||
|
QWidget *w = gv;
|
||||||
|
while (w) {
|
||||||
|
tabGame = qobject_cast<TabGame *>(w);
|
||||||
|
if (tabGame) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
w = w->parentWidget();
|
||||||
|
}
|
||||||
|
if (!tabGame) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: TabGame not found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractGame *game = tabGame->getGame();
|
||||||
|
if (!game) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no game";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int activePlayerId = game->getGameState()->getActivePlayer();
|
||||||
|
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(activePlayerId);
|
||||||
|
if (!player) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no active local player";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenInfo info;
|
||||||
|
info.name = tokenName;
|
||||||
|
info.color = tokenColor;
|
||||||
|
info.pt = tokenPt;
|
||||||
|
info.destroy = true;
|
||||||
|
info.faceDown = false;
|
||||||
|
|
||||||
|
player->getPlayerActions()->actCreateToken(info);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
qApp->processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playSelectItem(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
const QString text = arguments.trimmed();
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: needs the label of the item to select";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto *tree = qobject_cast<QTreeWidget *>(object)) {
|
||||||
|
QTreeWidgetItemIterator item(tree);
|
||||||
|
while (*item) {
|
||||||
|
if ((*item)->text(0) == text) {
|
||||||
|
tree->setCurrentItem(*item);
|
||||||
|
qApp->processEvents();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
++item;
|
||||||
|
}
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no tree item labelled" << text;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto *view = qobject_cast<QAbstractItemView *>(object)) {
|
||||||
|
const QModelIndex index = findModelIndexByText(view->model(), text);
|
||||||
|
if (!index.isValid()) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no item labelled" << text;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
view->setCurrentIndex(index);
|
||||||
|
qApp->processEvents();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: target is neither a tree widget nor an item view";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playTypeText(QObject *object, const QString &arguments)
|
||||||
|
{
|
||||||
|
auto *widget = qobject_cast<QWidget *>(object);
|
||||||
|
if (!widget) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "typeText: target is not a QWidget";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QChar character : arguments) {
|
||||||
|
const Qt::Key key = keyForCharacter(character);
|
||||||
|
const Qt::KeyboardModifiers modifiers = needsShift(character) ? Qt::ShiftModifier : Qt::NoModifier;
|
||||||
|
QKeyEvent press(QEvent::KeyPress, key, modifiers, QString(character));
|
||||||
|
QKeyEvent release(QEvent::KeyRelease, key, modifiers, QString(character));
|
||||||
|
qApp->notify(widget, &press);
|
||||||
|
qApp->notify(widget, &release);
|
||||||
|
}
|
||||||
|
qApp->processEvents();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pqCockatriceEventPlayer::playExpectProperty(QObject *object, const QString &arguments, bool &error)
|
||||||
|
{
|
||||||
|
if (!object) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty: no target object";
|
||||||
|
error = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = arguments.split(',');
|
||||||
|
if (parts.size() < 2) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "expectProperty: arguments must be 'propertyName,expectedValue', got:" << arguments;
|
||||||
|
error = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString propertyName = parts[0].trimmed();
|
||||||
|
const QString expected = parts.mid(1).join(',');
|
||||||
|
|
||||||
|
const QVariant actual = object->property(propertyName.toUtf8().constData());
|
||||||
|
if (!actual.isValid()) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
|
||||||
|
<< "expectProperty:" << object->objectName() << "has no property" << propertyName;
|
||||||
|
error = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual.toString() != expected) {
|
||||||
|
qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty:" << object->objectName() << propertyName << "is"
|
||||||
|
<< actual.toString() << "but the test expected" << expected;
|
||||||
|
error = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
91
cockatrice/src/game_graphics/pq_cockatrice_event_player.h
Normal file
91
cockatrice/src/game_graphics/pq_cockatrice_event_player.h
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#ifndef PQ_COCKATRICE_EVENT_PLAYER_H
|
||||||
|
#define PQ_COCKATRICE_EVENT_PLAYER_H
|
||||||
|
|
||||||
|
#include "pqWidgetEventPlayer.h"
|
||||||
|
|
||||||
|
#include <QModelIndex>
|
||||||
|
|
||||||
|
class CardItem;
|
||||||
|
class QAction;
|
||||||
|
class QGraphicsItem;
|
||||||
|
class QGraphicsScene;
|
||||||
|
class QGraphicsView;
|
||||||
|
class QMenu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom event player for Cockatrice game interactions.
|
||||||
|
*
|
||||||
|
* Card commands are resolution independent: the card is looked up in the
|
||||||
|
* QGraphicsScene by name and zone, then mouse events are sent at the card's
|
||||||
|
* current viewport position. That keeps recordings usable across screen sizes
|
||||||
|
* and window geometries.
|
||||||
|
*
|
||||||
|
* Commands:
|
||||||
|
* clickCard "cardName,zoneName"
|
||||||
|
* left press/release + double click — plays a card from hand, taps a
|
||||||
|
* permanent, opens the deck view entry, depending on the zone
|
||||||
|
* flipCard "cardName,zoneName"
|
||||||
|
* right-click → "Turn Over" (shorthand for cardAction)
|
||||||
|
* cardAction "cardName,zoneName,actionText"
|
||||||
|
* right-click the card and trigger the first context menu action whose
|
||||||
|
* label contains actionText; submenus are searched as well, so
|
||||||
|
* "Add counter (A)" works
|
||||||
|
* moveCard "cardName,srcZoneName,dstZoneName"
|
||||||
|
* left-drag the card onto the destination zone — plays a card, moves it
|
||||||
|
* to the graveyard/exile, puts it back into hand, ...
|
||||||
|
* drawArrow "srcCard,srcZone,dstCard,dstZone"
|
||||||
|
* right-drag an arrow from one card to another
|
||||||
|
* createToken "name,color,pt"
|
||||||
|
* create a token through the active local player, as the table context
|
||||||
|
* menu's "Create token" dialog would
|
||||||
|
* selectItem "item text"
|
||||||
|
* selects the first item of a tree/list widget whose label matches, for
|
||||||
|
* navigating the settings navigation tree or similar label-driven views
|
||||||
|
* typeText "some text"
|
||||||
|
* types the text into the widget one key event at a time — use it for
|
||||||
|
* deck lists and search boxes instead of one event per character
|
||||||
|
* expectProperty "propertyName,expectedValue"
|
||||||
|
* assertion: playback fails unless object->property(propertyName)
|
||||||
|
* equals expectedValue — for example "count,2" on the tab widget
|
||||||
|
*/
|
||||||
|
class pqCockatriceEventPlayer : public pqWidgetEventPlayer
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
typedef pqWidgetEventPlayer Superclass;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit pqCockatriceEventPlayer(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
using Superclass::playEvent;
|
||||||
|
|
||||||
|
bool playEvent(QObject *object, const QString &command, const QString &arguments, bool &error) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool playClickCard(QObject *object, const QString &arguments);
|
||||||
|
bool playFlipCard(QObject *object, const QString &arguments);
|
||||||
|
bool playCardAction(QObject *object, const QString &arguments);
|
||||||
|
bool playMoveCard(QObject *object, const QString &arguments);
|
||||||
|
bool playDrawArrow(QObject *object, const QString &arguments);
|
||||||
|
bool playCreateToken(QObject *object, const QString &arguments);
|
||||||
|
bool playSelectItem(QObject *object, const QString &arguments);
|
||||||
|
bool playTypeText(QObject *object, const QString &arguments);
|
||||||
|
bool playExpectProperty(QObject *object, const QString &arguments, bool &error);
|
||||||
|
|
||||||
|
/** @brief Triggers the context menu entry @p actionText after right-clicking @p card. */
|
||||||
|
bool invokeCardMenuAction(const char *command, QGraphicsView *view, CardItem *card, const QString &actionText);
|
||||||
|
|
||||||
|
static QGraphicsItem *findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName);
|
||||||
|
static QGraphicsItem *findZoneInScene(QGraphicsScene *scene, const QString &zoneName);
|
||||||
|
static QAction *findActionInMenu(QMenu *menu, const QString &actionText);
|
||||||
|
static QMenu *visiblePopupMenu();
|
||||||
|
|
||||||
|
/** @brief Viewport position of the item's centre, as the mouse events have to be sent. */
|
||||||
|
static QPoint viewportPoint(QGraphicsView *view, const QGraphicsItem *item);
|
||||||
|
static void sendMouseEvent(QGraphicsView *view,
|
||||||
|
QEvent::Type type,
|
||||||
|
const QPoint &pos,
|
||||||
|
Qt::MouseButton button,
|
||||||
|
Qt::MouseButtons buttons);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PQ_COCKATRICE_EVENT_PLAYER_H
|
||||||
|
|
@ -28,12 +28,16 @@ class QWidget;
|
||||||
*/
|
*/
|
||||||
AbstractDlgDeckTextEdit::AbstractDlgDeckTextEdit(QWidget *parent) : QDialog(parent)
|
AbstractDlgDeckTextEdit::AbstractDlgDeckTextEdit(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("dlgDeckTextEdit");
|
||||||
|
|
||||||
contentsEdit = new QPlainTextEdit;
|
contentsEdit = new QPlainTextEdit;
|
||||||
|
contentsEdit->setObjectName("contentsEdit");
|
||||||
|
|
||||||
refreshButton = new QPushButton(tr("&Refresh"));
|
refreshButton = new QPushButton(tr("&Refresh"));
|
||||||
connect(refreshButton, &QPushButton::clicked, this, &AbstractDlgDeckTextEdit::actRefresh);
|
connect(refreshButton, &QPushButton::clicked, this, &AbstractDlgDeckTextEdit::actRefresh);
|
||||||
|
|
||||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
buttonBox->setObjectName("buttonBox");
|
||||||
buttonBox->addButton(refreshButton, QDialogButtonBox::ActionRole);
|
buttonBox->addButton(refreshButton, QDialogButtonBox::ActionRole);
|
||||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &AbstractDlgDeckTextEdit::actOK);
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &AbstractDlgDeckTextEdit::actOK);
|
||||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &AbstractDlgDeckTextEdit::reject);
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &AbstractDlgDeckTextEdit::reject);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@ class QWidget;
|
||||||
|
|
||||||
DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
|
DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("localGameDialog");
|
||||||
|
|
||||||
numberPlayersLabel = new QLabel(tr("Players:"), this);
|
numberPlayersLabel = new QLabel(tr("Players:"), this);
|
||||||
numberPlayersEdit = new QSpinBox(this);
|
numberPlayersEdit = new QSpinBox(this);
|
||||||
|
numberPlayersEdit->setObjectName("numberPlayersEdit");
|
||||||
numberPlayersEdit->setMinimum(1);
|
numberPlayersEdit->setMinimum(1);
|
||||||
numberPlayersEdit->setMaximum(8);
|
numberPlayersEdit->setMaximum(8);
|
||||||
numberPlayersEdit->setValue(1);
|
numberPlayersEdit->setValue(1);
|
||||||
|
|
@ -28,10 +31,12 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
|
||||||
generalGrid->addWidget(numberPlayersLabel, 0, 0);
|
generalGrid->addWidget(numberPlayersLabel, 0, 0);
|
||||||
generalGrid->addWidget(numberPlayersEdit, 0, 1);
|
generalGrid->addWidget(numberPlayersEdit, 0, 1);
|
||||||
generalGroupBox = new QGroupBox(tr("General"), this);
|
generalGroupBox = new QGroupBox(tr("General"), this);
|
||||||
|
generalGroupBox->setObjectName("generalGroupBox");
|
||||||
generalGroupBox->setLayout(generalGrid);
|
generalGroupBox->setLayout(generalGrid);
|
||||||
|
|
||||||
startingLifeTotalLabel = new QLabel(tr("Starting life total:"), this);
|
startingLifeTotalLabel = new QLabel(tr("Starting life total:"), this);
|
||||||
startingLifeTotalEdit = new QSpinBox(this);
|
startingLifeTotalEdit = new QSpinBox(this);
|
||||||
|
startingLifeTotalEdit->setObjectName("startingLifeTotalEdit");
|
||||||
startingLifeTotalEdit->setMinimum(-999999999);
|
startingLifeTotalEdit->setMinimum(-999999999);
|
||||||
startingLifeTotalEdit->setMaximum(999999999);
|
startingLifeTotalEdit->setMaximum(999999999);
|
||||||
startingLifeTotalEdit->setValue(20);
|
startingLifeTotalEdit->setValue(20);
|
||||||
|
|
@ -42,11 +47,14 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent)
|
||||||
gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0);
|
gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0);
|
||||||
gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1);
|
gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1);
|
||||||
gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this);
|
gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this);
|
||||||
|
gameSetupOptionsGroupBox->setObjectName("gameSetupOptionsGroupBox");
|
||||||
gameSetupOptionsGroupBox->setLayout(gameSetupGrid);
|
gameSetupOptionsGroupBox->setLayout(gameSetupGrid);
|
||||||
|
|
||||||
rememberSettingsCheckBox = new QCheckBox(tr("Remember settings"), this);
|
rememberSettingsCheckBox = new QCheckBox(tr("Remember settings"), this);
|
||||||
|
rememberSettingsCheckBox->setObjectName("rememberSettingsCheckBox");
|
||||||
|
|
||||||
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
buttonBox->setObjectName("buttonBox");
|
||||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgLocalGameOptions::actOK);
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgLocalGameOptions::actOK);
|
||||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgLocalGameOptions::reject);
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgLocalGameOptions::reject);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ class Message;
|
||||||
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
|
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
|
||||||
: Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr)
|
: Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr)
|
||||||
{
|
{
|
||||||
|
setObjectName("TabGame");
|
||||||
// THIS CTOR IS USED ON REPLAY
|
// THIS CTOR IS USED ON REPLAY
|
||||||
game = new Replay(this, _replay, tabSupervisor->getIsLocalGame(), &SettingsCache::instance().userInterface(),
|
game = new Replay(this, _replay, tabSupervisor->getIsLocalGame(), &SettingsCache::instance().userInterface(),
|
||||||
new SettingsCardPreferenceProvider(this));
|
new SettingsCardPreferenceProvider(this));
|
||||||
|
|
@ -116,6 +117,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
|
||||||
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
|
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
|
||||||
|
|
||||||
mainWidget = new QStackedWidget(this);
|
mainWidget = new QStackedWidget(this);
|
||||||
|
mainWidget->setObjectName("mainWidget");
|
||||||
mainWidget->addWidget(deckViewContainerWidget);
|
mainWidget->addWidget(deckViewContainerWidget);
|
||||||
mainWidget->addWidget(gamePlayAreaWidget);
|
mainWidget->addWidget(gamePlayAreaWidget);
|
||||||
setCentralWidget(mainWidget);
|
setCentralWidget(mainWidget);
|
||||||
|
|
@ -144,6 +146,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
||||||
const QMap<int, QString> &_roomGameTypes)
|
const QMap<int, QString> &_roomGameTypes)
|
||||||
: Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager())
|
: Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager())
|
||||||
{
|
{
|
||||||
|
setObjectName("TabGame");
|
||||||
// THIS CTOR IS USED ON GAMES
|
// THIS CTOR IS USED ON GAMES
|
||||||
game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes,
|
game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes,
|
||||||
&SettingsCache::instance().userInterface(), new SettingsCardPreferenceProvider(this));
|
&SettingsCache::instance().userInterface(), new SettingsCardPreferenceProvider(this));
|
||||||
|
|
@ -160,6 +163,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
||||||
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
|
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
|
||||||
|
|
||||||
mainWidget = new QStackedWidget(this);
|
mainWidget = new QStackedWidget(this);
|
||||||
|
mainWidget->setObjectName("mainWidget");
|
||||||
mainWidget->addWidget(deckViewContainerWidget);
|
mainWidget->addWidget(deckViewContainerWidget);
|
||||||
mainWidget->addWidget(gamePlayAreaWidget);
|
mainWidget->addWidget(gamePlayAreaWidget);
|
||||||
mainWidget->setContentsMargins(0, 0, 0, 0);
|
mainWidget->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
@ -1118,6 +1122,7 @@ void TabGame::createMenuItems()
|
||||||
connect(aFocusChat, &QAction::triggered, sayEdit, qOverload<>(&LineEditCompleter::setFocus));
|
connect(aFocusChat, &QAction::triggered, sayEdit, qOverload<>(&LineEditCompleter::setFocus));
|
||||||
|
|
||||||
phasesMenu = new TearOffMenu(this);
|
phasesMenu = new TearOffMenu(this);
|
||||||
|
phasesMenu->setObjectName("phasesMenu");
|
||||||
|
|
||||||
for (int i = 0; i < phasesToolbar->phaseCount(); ++i) {
|
for (int i = 0; i < phasesToolbar->phaseCount(); ++i) {
|
||||||
auto *temp = new QAction(QString(), this);
|
auto *temp = new QAction(QString(), this);
|
||||||
|
|
@ -1128,12 +1133,16 @@ void TabGame::createMenuItems()
|
||||||
|
|
||||||
phasesMenu->addSeparator();
|
phasesMenu->addSeparator();
|
||||||
phasesMenu->addAction(aNextPhase);
|
phasesMenu->addAction(aNextPhase);
|
||||||
|
aNextPhase->setObjectName("aNextPhase");
|
||||||
phasesMenu->addAction(aNextPhaseAction);
|
phasesMenu->addAction(aNextPhaseAction);
|
||||||
|
aNextPhaseAction->setObjectName("aNextPhaseAction");
|
||||||
|
|
||||||
gameMenu = new QMenu(this);
|
gameMenu = new QMenu(this);
|
||||||
|
gameMenu->setObjectName("gameMenu");
|
||||||
playersSeparator = gameMenu->addSeparator();
|
playersSeparator = gameMenu->addSeparator();
|
||||||
gameMenu->addMenu(phasesMenu);
|
gameMenu->addMenu(phasesMenu);
|
||||||
gameMenu->addAction(aNextTurn);
|
gameMenu->addAction(aNextTurn);
|
||||||
|
aNextTurn->setObjectName("aNextTurn");
|
||||||
gameMenu->addAction(aReverseTurn);
|
gameMenu->addAction(aReverseTurn);
|
||||||
gameMenu->addSeparator();
|
gameMenu->addSeparator();
|
||||||
gameMenu->addAction(aRemoveLocalArrows);
|
gameMenu->addAction(aRemoveLocalArrows);
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
#include "widgets/settings_page/abstract_settings_page.h"
|
#include "widgets/settings_page/abstract_settings_page.h"
|
||||||
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h"
|
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h"
|
||||||
#ifdef WITH_GUI_TEST
|
#ifdef WITH_GUI_TEST
|
||||||
|
#include "../game_graphics/pq_cockatrice_event_player.h"
|
||||||
#include "pqEventObserver.h"
|
#include "pqEventObserver.h"
|
||||||
#include "pqEventSource.h"
|
#include "pqEventSource.h"
|
||||||
#include "pqEventTypes.h"
|
#include "pqEventTypes.h"
|
||||||
|
|
@ -689,6 +690,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
testUtility->setRecordWithDialog(true);
|
testUtility->setRecordWithDialog(true);
|
||||||
testUtility->addEventObserver("xml", new XMLEventObserver(this));
|
testUtility->addEventObserver("xml", new XMLEventObserver(this));
|
||||||
testUtility->addEventSource("xml", new XMLEventSource(this));
|
testUtility->addEventSource("xml", new XMLEventSource(this));
|
||||||
|
testUtility->eventPlayer()->addWidgetEventPlayer(new pqCockatriceEventPlayer(this));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue