* UI: choose modes dialog improves:

* Added hotkeys to select options (1-9 for choice, SPACE/ENTER for done, ESC for cancel);
 * "Up to" modes choose dialog - added "done" button in dialog;
 * "Up to" modes choose dialog - fixed that user can't cancel if already selected one mode;
 * Added extra info about source object, selected and remaining modes to select, ability number for hotkey;
 * Fixed that mode choose dialog doesn't close on cancel (#6199);
This commit is contained in:
Oleg Agafonov 2020-01-18 06:30:44 +04:00
parent cf97b9e6c7
commit 8add25fa12
6 changed files with 179 additions and 40 deletions

View file

@ -1,7 +1,9 @@
package mage.client.components.ability; package mage.client.components.ability;
import mage.abilities.Modes;
import mage.client.SessionHandler; import mage.client.SessionHandler;
import mage.client.dialog.MageDialog; import mage.client.dialog.MageDialog;
import mage.client.game.GamePanel;
import mage.client.util.ImageHelper; import mage.client.util.ImageHelper;
import mage.remote.Session; import mage.remote.Session;
import mage.view.AbilityPickerView; import mage.view.AbilityPickerView;
@ -22,7 +24,7 @@ import java.util.*;
/** /**
* Dialog for choosing abilities. * Dialog for choosing abilities.
* *
* @author nantuko * @author nantuko, JayDi85
*/ */
public class AbilityPicker extends JXPanel implements MouseWheelListener { public class AbilityPicker extends JXPanel implements MouseWheelListener {
@ -41,7 +43,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
private BackgroundPainter mwPanelPainter; private BackgroundPainter mwPanelPainter;
private JScrollPane jScrollPane2; private JScrollPane jScrollPane2;
private JTextField title; private JLabel title;
private Image rightImage; private Image rightImage;
private Image rightImageHovered; private Image rightImageHovered;
@ -68,9 +70,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
public AbilityPicker(List<Object> choices, String message) { public AbilityPicker(List<Object> choices, String message) {
this.choices = choices; this.choices = choices;
setSize(DIALOG_WIDTH, DIALOG_HEIGHT); setSize(DIALOG_WIDTH, DIALOG_HEIGHT);
if (message != null) { setMessageAndPrepare(message);
this.message = message + " (single-click)";
}
initComponents(); initComponents();
jScrollPane2.setOpaque(false); jScrollPane2.setOpaque(false);
jScrollPane2.getViewport().setOpaque(false); jScrollPane2.getViewport().setOpaque(false);
@ -80,7 +80,6 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
} }
public void init(UUID gameId) { public void init(UUID gameId) {
this.gameId = gameId; this.gameId = gameId;
} }
@ -93,11 +92,17 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
public void show(AbilityPickerView choices, Point p) { public void show(AbilityPickerView choices, Point p) {
this.choices = new ArrayList<>(); this.choices = new ArrayList<>();
this.selected = true; // to stop previous modal this.selected = true; // to stop previous modal
setMessageAndPrepare(choices.getMessage());
// if not cancel from server then add own
boolean wasCancelButton = false;
for (Map.Entry<UUID, String> choice : choices.getChoices().entrySet()) { for (Map.Entry<UUID, String> choice : choices.getChoices().entrySet()) {
wasCancelButton = wasCancelButton || choice.getKey().equals(Modes.CHOOSE_OPTION_CANCEL_ID);
this.choices.add(new AbilityPickerAction(choice.getKey(), choice.getValue())); this.choices.add(new AbilityPickerAction(choice.getKey(), choice.getValue()));
} }
this.choices.add(new AbilityPickerAction(null, "Cancel")); if (!wasCancelButton) {
this.choices.add(new AbilityPickerAction(null, "Cancel"));
}
show(this.choices); show(this.choices);
} }
@ -109,6 +114,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
rows.setListData(this.choices.toArray()); rows.setListData(this.choices.toArray());
this.rows.setSelectedIndex(0); this.rows.setSelectedIndex(0);
this.selected = false; // back to false - waiting for selection this.selected = false; // back to false - waiting for selection
this.title.setText(this.message);
setVisible(true); setVisible(true);
MageDialog.makeWindowCentered(this, DIALOG_WIDTH, DIALOG_HEIGHT); MageDialog.makeWindowCentered(this, DIALOG_WIDTH, DIALOG_HEIGHT);
@ -116,30 +122,17 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
} }
private void initComponents() { private void initComponents() {
JLabel jLabel1;
JLabel jLabel3;
Color textColor = Color.white; Color textColor = Color.white;
mwPanelPainter = new BackgroundPainter(); mwPanelPainter = new BackgroundPainter();
jLabel1 = new JLabel();
jLabel3 = new JLabel();
title = new JTextField();
jScrollPane2 = new JScrollPane(); jScrollPane2 = new JScrollPane();
setBackground(textColor); setBackground(textColor);
setBackgroundPainter(mwPanelPainter); setBackgroundPainter(mwPanelPainter);
jLabel1.setFont(new Font("Times New Roman", 1, 18));
jLabel1.setForeground(textColor);
jLabel1.setText(message);
jLabel3.setForeground(textColor); title = new JLabel();
jLabel3.setHorizontalAlignment(SwingConstants.TRAILING); title.setFont(new Font("Times New Roman", 1, 15));
jLabel3.setText("Selected:"); title.setForeground(textColor);
title.setText(message);
title.setFont(new Font("Tahoma", 1, 11));
title.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
jScrollPane2.setBorder(null); jScrollPane2.setBorder(null);
jScrollPane2.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); jScrollPane2.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
@ -161,6 +154,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
rows.setMinimumSize(new Dimension(67, 16)); rows.setMinimumSize(new Dimension(67, 16));
rows.setOpaque(false); rows.setOpaque(false);
// mouse actions
rows.addMouseListener(new MouseAdapter() { rows.addMouseListener(new MouseAdapter() {
@Override @Override
public void mousePressed(MouseEvent evt) { public void mousePressed(MouseEvent evt) {
@ -174,6 +168,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
rows.setBorder(BorderFactory.createEmptyBorder()); rows.setBorder(BorderFactory.createEmptyBorder());
rows.addMouseWheelListener(this); rows.addMouseWheelListener(this);
jScrollPane2.setViewportView(rows); jScrollPane2.setViewportView(rows);
jScrollPane2.setViewportBorder(BorderFactory.createEmptyBorder()); jScrollPane2.setViewportBorder(BorderFactory.createEmptyBorder());
@ -184,7 +179,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
GroupLayout.TRAILING, GroupLayout.TRAILING,
layout.createSequentialGroup().addContainerGap().add( layout.createSequentialGroup().addContainerGap().add(
layout.createParallelGroup(GroupLayout.TRAILING).add(GroupLayout.LEADING, jScrollPane2, GroupLayout.DEFAULT_SIZE, 422, Short.MAX_VALUE).add(GroupLayout.LEADING, layout.createParallelGroup(GroupLayout.TRAILING).add(GroupLayout.LEADING, jScrollPane2, GroupLayout.DEFAULT_SIZE, 422, Short.MAX_VALUE).add(GroupLayout.LEADING,
layout.createSequentialGroup().add(jLabel1).addPreferredGap(LayoutStyle.RELATED, 175, Short.MAX_VALUE).add(1, 1, 1)).add( layout.createSequentialGroup().add(title).addPreferredGap(LayoutStyle.RELATED, 175, Short.MAX_VALUE).add(1, 1, 1)).add(
GroupLayout.LEADING, GroupLayout.LEADING,
layout.createSequentialGroup().add(layout.createParallelGroup(GroupLayout.LEADING) layout.createSequentialGroup().add(layout.createParallelGroup(GroupLayout.LEADING)
) )
@ -197,7 +192,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.LEADING).add( layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.LEADING).add(
layout.createSequentialGroup().add( layout.createSequentialGroup().add(
layout.createParallelGroup(GroupLayout.LEADING).add( layout.createParallelGroup(GroupLayout.LEADING).add(
layout.createSequentialGroup().add(jLabel1, GroupLayout.PREFERRED_SIZE, 36, GroupLayout.PREFERRED_SIZE) layout.createSequentialGroup().add(title, GroupLayout.PREFERRED_SIZE, 72, GroupLayout.PREFERRED_SIZE)
.add(5, 5, 5) .add(5, 5, 5)
.add( .add(
layout.createParallelGroup(GroupLayout.BASELINE) layout.createParallelGroup(GroupLayout.BASELINE)
@ -463,4 +458,102 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener {
log.error("Couldn't cancel choose dialog: " + e, e); log.error("Couldn't cancel choose dialog: " + e, e);
} }
} }
private void setMessageAndPrepare(String message) {
if (message != null) {
this.message = message + " (single-click or hotkeys)";
} else {
this.message = DEFAULT_MESSAGE;
}
this.message = "<html>" + this.message;
}
private void tryChoiceDone() {
// done by keyboard
if (!isVisible() || choices == null) {
return;
}
for (Object obj : choices) {
AbilityPickerAction action = (AbilityPickerAction) obj;
if (Modes.CHOOSE_OPTION_DONE_ID.equals(action.id)) {
action.actionPerformed(null);
break;
}
}
}
private void tryChoiceCancel() {
// cancel by keyboard
if (!isVisible() || choices == null) {
return;
}
for (Object obj : choices) {
AbilityPickerAction action = (AbilityPickerAction) obj;
if (Modes.CHOOSE_OPTION_DONE_ID.equals(action.id)) {
action.actionPerformed(null);
break;
}
}
}
private void tryChoiceOption(int choiceNumber) {
// choice by keyboard
if (!isVisible() || choices == null) {
return;
}
String need = choiceNumber + ".";
for (Object obj : choices) {
AbilityPickerAction action = (AbilityPickerAction) obj;
if (action.toString().startsWith(need)) {
action.actionPerformed(null);
break;
}
}
}
public void injectHotkeys(GamePanel panel, String commandsPrefix) {
// TODO: fix that GamePanel recive imput from any place, not only active (e.g. F9 works from lobby)
int c = JComponent.WHEN_IN_FOCUSED_WINDOW;
// choice keys
Map<Integer, Integer> numbers = new HashMap<>();
numbers.put(KeyEvent.VK_1, 1);
numbers.put(KeyEvent.VK_2, 2);
numbers.put(KeyEvent.VK_3, 3);
numbers.put(KeyEvent.VK_4, 4);
numbers.put(KeyEvent.VK_5, 5);
numbers.put(KeyEvent.VK_6, 6);
numbers.put(KeyEvent.VK_7, 7);
numbers.put(KeyEvent.VK_8, 8);
numbers.put(KeyEvent.VK_9, 9);
numbers.forEach((vk, num) -> {
KeyStroke ks = KeyStroke.getKeyStroke(vk, 0);
panel.getInputMap(c).put(ks, commandsPrefix + "_CHOOSE_" + num);
panel.getActionMap().put(commandsPrefix + "_CHOOSE_" + num, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
tryChoiceOption(num);
}
});
});
// done key (space, enter)
panel.getInputMap(c).put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), commandsPrefix + "_CHOOSE_DONE");
panel.getInputMap(c).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), commandsPrefix + "_CHOOSE_DONE");
panel.getActionMap().put(commandsPrefix + "_CHOOSE_DONE", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
tryChoiceDone();
}
});
// cancel key (esc)
panel.getInputMap(c).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), commandsPrefix + "_CHOOSE_CANCEL");
panel.getActionMap().put(commandsPrefix + "_CHOOSE_CANCEL", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
tryChoiceCancel();
}
});
}
} }

View file

@ -394,7 +394,6 @@ public final class GamePanel extends javax.swing.JPanel {
this.feedbackPanel.init(gameId); this.feedbackPanel.init(gameId);
this.feedbackPanel.clear(); this.feedbackPanel.clear();
this.abilityPicker.init(gameId); this.abilityPicker.init(gameId);
this.btnConcede.setVisible(true); this.btnConcede.setVisible(true);
this.btnStopWatching.setVisible(false); this.btnStopWatching.setVisible(false);
this.btnSwitchHands.setVisible(false); this.btnSwitchHands.setVisible(false);
@ -1399,6 +1398,8 @@ public final class GamePanel extends javax.swing.JPanel {
} }
public void select(String message, GameView gameView, int messageId, Map<String, Serializable> options) { public void select(String message, GameView gameView, int messageId, Map<String, Serializable> options) {
this.abilityPicker.setVisible(false);
holdingPriority = false; holdingPriority = false;
txtHoldPriority.setVisible(false); txtHoldPriority.setVisible(false);
setMenuStates( setMenuStates(
@ -1464,6 +1465,7 @@ public final class GamePanel extends javax.swing.JPanel {
} }
private void hideAll() { private void hideAll() {
this.abilityPicker.setVisible(false);
ActionCallback callback = Plugins.instance.getActionCallback(); ActionCallback callback = Plugins.instance.getActionCallback();
((MageActionCallback) callback).hideGameUpdate(gameId); ((MageActionCallback) callback).hideGameUpdate(gameId);
} }
@ -1900,6 +1902,9 @@ public final class GamePanel extends javax.swing.JPanel {
} }
}); });
// special hotkeys for custom rendered dialogs without focus
this.abilityPicker.injectHotkeys(this, "ABILITY_PICKER");
final BasicSplitPaneUI myUi = (BasicSplitPaneUI) jSplitPane0.getUI(); final BasicSplitPaneUI myUi = (BasicSplitPaneUI) jSplitPane0.getUI();
final BasicSplitPaneDivider divider = myUi.getDivider(); final BasicSplitPaneDivider divider = myUi.getDivider();
final JButton upArrowButton = (JButton) divider.getComponent(0); final JButton upArrowButton = (JButton) divider.getComponent(0);
@ -2268,6 +2273,10 @@ public final class GamePanel extends javax.swing.JPanel {
for (ComponentListener cl : this.getComponentListeners()) { for (ComponentListener cl : this.getComponentListeners()) {
this.removeComponentListener(cl); this.removeComponentListener(cl);
} }
for (KeyListener kl : this.getKeyListeners()) {
this.removeKeyListener(kl);
}
} }
private void btnConcedeActionPerformed(java.awt.event.ActionEvent evt) { private void btnConcedeActionPerformed(java.awt.event.ActionEvent evt) {
@ -2711,5 +2720,4 @@ class ReplayTask extends SwingWorker<Void, Collection<MatchView>> {
} catch (CancellationException ex) { } catch (CancellationException ex) {
} }
} }
} }

View file

@ -1,15 +1,14 @@
package mage.view; package mage.view;
import mage.abilities.Ability;
import java.io.Serializable; import java.io.Serializable;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import mage.abilities.Ability;
/** /**
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class AbilityPickerView implements Serializable { public class AbilityPickerView implements Serializable {
@ -17,6 +16,7 @@ public class AbilityPickerView implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Map<UUID, String> choices = new LinkedHashMap<>(); private Map<UUID, String> choices = new LinkedHashMap<>();
private String message = null;
public AbilityPickerView(String objectName, List<? extends Ability> abilities) { public AbilityPickerView(String objectName, List<? extends Ability> abilities) {
for (Ability ability : abilities) { for (Ability ability : abilities) {
@ -32,11 +32,16 @@ public class AbilityPickerView implements Serializable {
} }
} }
public AbilityPickerView(Map<UUID, String> modes) { public AbilityPickerView(Map<UUID, String> modes, String message) {
this.choices = modes; this.choices = modes;
this.message = message;
} }
public Map<UUID, String> getChoices() { public Map<UUID, String> getChoices() {
return choices; return choices;
} }
public String getMessage() {
return message;
}
} }

View file

@ -2040,10 +2040,14 @@ public class HumanPlayer extends PlayerImpl {
} }
if (modes.size() > 1) { if (modes.size() > 1) {
// done option for up to choices
boolean canEndChoice = modes.getSelectedModes().size() >= modes.getMinModes();
MageObject obj = game.getObject(source.getSourceId()); MageObject obj = game.getObject(source.getSourceId());
Map<UUID, String> modeMap = new LinkedHashMap<>(); Map<UUID, String> modeMap = new LinkedHashMap<>();
int modeIndex = 0;
AvailableModes: AvailableModes:
for (Mode mode : modes.getAvailableModes(source, game)) { for (Mode mode : modes.getAvailableModes(source, game)) {
modeIndex++;
int timesSelected = modes.getSelectedStats(mode.getId()); int timesSelected = modes.getSelectedStats(mode.getId());
for (UUID selectedModeId : modes.getSelectedModes()) { for (UUID selectedModeId : modes.getSelectedModes()) {
Mode selectedMode = modes.get(selectedModeId); Mode selectedMode = modes.get(selectedModeId);
@ -2068,18 +2072,31 @@ public class HumanPlayer extends PlayerImpl {
modeText = "(selected " + timesSelected + "x) " + modeText; modeText = "(selected " + timesSelected + "x) " + modeText;
} }
} }
modeMap.put(mode.getId(), modeText); modeMap.put(mode.getId(), modeIndex + ". " + modeText);
} }
} }
if (!modeMap.isEmpty()) { if (!modeMap.isEmpty()) {
// can done for up to
if (canEndChoice) {
modeMap.put(Modes.CHOOSE_OPTION_DONE_ID, "Done");
}
modeMap.put(Modes.CHOOSE_OPTION_CANCEL_ID, "Cancel");
boolean done = false; boolean done = false;
while (!done && canRespond()) { while (!done && canRespond()) {
String message = "Choose mode (selected " + modes.getSelectedModes().size() + " of " + modes.getMaxModes()
+ ", min " + modes.getMinModes() + ")";
if (obj != null) {
message = message + "<br>" + obj.getLogName();
}
updateGameStatePriority("chooseMode", game); updateGameStatePriority("chooseMode", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetModeEvent(playerId, "Choose Mode", modeMap); game.fireGetModeEvent(playerId, message, modeMap);
} }
waitForResponse(game); waitForResponse(game);
@ -2091,9 +2108,21 @@ public class HumanPlayer extends PlayerImpl {
return mode; return mode;
} }
} }
} else if (modes.getSelectedModes().size() >= modes.getMinModes()) {
/* let the player cancel mode selection if they do not need to select any further modes */ // end choice by done option in ability pickup dialog
done = true; if (canEndChoice && Modes.CHOOSE_OPTION_DONE_ID.equals(response.getUUID())) {
done = true;
}
// cancel choice (remove all selections)
if (Modes.CHOOSE_OPTION_CANCEL_ID.equals(response.getUUID())) {
modes.getSelectedModes().clear();
return null;
}
} else if (canEndChoice) {
// end choice by done button in feedback panel
// disable after done option implemented
// done = true;
} }
if (source.getAbilityType() != AbilityType.TRIGGERED) { if (source.getAbilityType() != AbilityType.TRIGGERED) {
done = true; done = true;

View file

@ -207,7 +207,7 @@ public class GameController implements GameCallback {
choosePile(event.getPlayerId(), event.getMessage(), event.getPile1(), event.getPile2()); choosePile(event.getPlayerId(), event.getMessage(), event.getPile1(), event.getPile2());
break; break;
case CHOOSE_MODE: case CHOOSE_MODE:
chooseMode(event.getPlayerId(), event.getModes()); chooseMode(event.getPlayerId(), event.getModes(), event.getMessage());
break; break;
case CHOOSE_CHOICE: case CHOOSE_CHOICE:
chooseChoice(event.getPlayerId(), event.getChoice()); chooseChoice(event.getPlayerId(), event.getChoice());
@ -769,8 +769,8 @@ public class GameController implements GameCallback {
perform(playerId, playerId1 -> getGameSession(playerId1).choosePile(message, new CardsView(pile1), new CardsView(pile2))); perform(playerId, playerId1 -> getGameSession(playerId1).choosePile(message, new CardsView(pile1), new CardsView(pile2)));
} }
private synchronized void chooseMode(UUID playerId, final Map<UUID, String> modes) throws MageException { private synchronized void chooseMode(UUID playerId, final Map<UUID, String> modes, final String message) throws MageException {
perform(playerId, playerId1 -> getGameSession(playerId1).chooseAbility(new AbilityPickerView(modes))); perform(playerId, playerId1 -> getGameSession(playerId1).chooseAbility(new AbilityPickerView(modes, message)));
} }
private synchronized void chooseChoice(UUID playerId, final Choice choice) throws MageException { private synchronized void chooseChoice(UUID playerId, final Choice choice) throws MageException {

View file

@ -18,6 +18,10 @@ import java.util.*;
*/ */
public class Modes extends LinkedHashMap<UUID, Mode> { public class Modes extends LinkedHashMap<UUID, Mode> {
// choose ID for options in ability/mode picker dialogs
public static final UUID CHOOSE_OPTION_DONE_ID = UUID.fromString("33e72ad6-17ae-4bfb-a097-6e7aa06b49e9");
public static final UUID CHOOSE_OPTION_CANCEL_ID = UUID.fromString("0125bd0c-5610-4eba-bc80-fc6d0a7b9de6");
private Mode currentMode; // the current mode of the selected modes private Mode currentMode; // the current mode of the selected modes
private final List<UUID> selectedModes = new ArrayList<>(); // all selected modes (this + duplicate) private final List<UUID> selectedModes = new ArrayList<>(); // all selected modes (this + duplicate)
private final Map<UUID, Mode> duplicateModes = new LinkedHashMap<>(); // for 2x selects: copy mode and put it to duplicate list private final Map<UUID, Mode> duplicateModes = new LinkedHashMap<>(); // for 2x selects: copy mode and put it to duplicate list