diff --git a/Mage.Client/src/main/java/mage/client/cards/VirtualCardInfo.java b/Mage.Client/src/main/java/mage/client/cards/VirtualCardInfo.java
index 90133395906..270371b1459 100644
--- a/Mage.Client/src/main/java/mage/client/cards/VirtualCardInfo.java
+++ b/Mage.Client/src/main/java/mage/client/cards/VirtualCardInfo.java
@@ -90,6 +90,10 @@ public class VirtualCardInfo {
data.setTooltipDelay(tooltipDelay);
}
+ public CardView getCardView() {
+ return this.cardView;
+ }
+
public boolean prepared() {
return this.cardView != null
&& this.cardComponent != null
diff --git a/Mage.Client/src/main/java/mage/client/chat/ChatPanelBasic.java b/Mage.Client/src/main/java/mage/client/chat/ChatPanelBasic.java
index c90dd861203..81ee956b31d 100644
--- a/Mage.Client/src/main/java/mage/client/chat/ChatPanelBasic.java
+++ b/Mage.Client/src/main/java/mage/client/chat/ChatPanelBasic.java
@@ -409,7 +409,7 @@ public class ChatPanelBasic extends javax.swing.JPanel {
}
public void enableHyperlinks() {
- txtConversation.enableHyperlinks();
+ txtConversation.enableHyperlinksAndCardPopups();
}
private void txtMessageKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_txtMessageKeyTyped
diff --git a/Mage.Client/src/main/java/mage/client/components/ColorPane.java b/Mage.Client/src/main/java/mage/client/components/ColorPane.java
index b39562180a2..25d11cb1aea 100644
--- a/Mage.Client/src/main/java/mage/client/components/ColorPane.java
+++ b/Mage.Client/src/main/java/mage/client/components/ColorPane.java
@@ -7,6 +7,7 @@ import mage.client.cards.VirtualCardInfo;
import mage.client.dialog.PreferencesDialog;
import mage.game.command.Plane;
import mage.util.CardUtil;
+import mage.util.GameLog;
import mage.utils.ThreadUtils;
import mage.view.CardView;
import mage.view.PlaneView;
@@ -87,6 +88,8 @@ public class ColorPane extends JEditorPane {
if (e.getEventType() == EventType.ENTERED) {
CardView cardView = null;
+ // TODO: add real game object first
+
// card
CardInfo card = CardRepository.instance.findCards(cardName).stream().findFirst().orElse(null);
if (card != null) {
@@ -144,19 +147,20 @@ public class ColorPane extends JEditorPane {
}
@Override
- public void setText(String string) {
- super.setText(string);
+ public void setText(String text) {
+ // must use append to enable popup/hyperlinks support
+ super.setText("");
+ append(text);
}
public void append(String text) {
try {
if (hyperlinkEnabled) {
- text = text.replaceAll("(]*>([^<]*)) (\\[[0-9a-fA-F]*\\])", "$1 $3");
+ text = GameLog.injectPopupSupport(text);
}
kit.insertHTML(doc, doc.getLength(), text, 0, 0, null);
int len = getDocument().getLength();
setCaretPosition(len);
-
} catch (Exception e) {
e.printStackTrace();
}
@@ -182,7 +186,7 @@ public class ColorPane extends JEditorPane {
super.paintChildren(g);
}
- public void enableHyperlinks() {
+ public void enableHyperlinksAndCardPopups() {
hyperlinkEnabled = true;
addHyperlinkHandlers();
}
diff --git a/Mage.Client/src/main/java/mage/client/components/MageDesktopIconifySupport.java b/Mage.Client/src/main/java/mage/client/components/MageDesktopIconifySupport.java
new file mode 100644
index 00000000000..336c620defb
--- /dev/null
+++ b/Mage.Client/src/main/java/mage/client/components/MageDesktopIconifySupport.java
@@ -0,0 +1,17 @@
+package mage.client.components;
+
+/**
+ * GUI: support windows mimize (iconify) by double clicks
+ *
+ * @author JayDi85
+ */
+public interface MageDesktopIconifySupport {
+
+ /**
+ * Window's header width after minimized to icon
+ */
+ default int getDesktopIconWidth() {
+ return 250;
+ }
+
+}
diff --git a/Mage.Client/src/main/java/mage/client/components/MageDesktopManager.java b/Mage.Client/src/main/java/mage/client/components/MageDesktopManager.java
index ecfd642ab72..59201c33981 100644
--- a/Mage.Client/src/main/java/mage/client/components/MageDesktopManager.java
+++ b/Mage.Client/src/main/java/mage/client/components/MageDesktopManager.java
@@ -3,31 +3,33 @@ package mage.client.components;
import java.awt.BorderLayout;
import javax.swing.*;
+import mage.client.dialog.CardHintsHelperDialog;
import mage.client.dialog.CardInfoWindowDialog;
/**
+ * GUI: helper class to improve work with desktop frames
*
* @author LevelX2
*/
public class MageDesktopManager extends DefaultDesktopManager {
- static final int DESKTOP_ICON_WIDTH = 250;
-
@Override
public void iconifyFrame(JInternalFrame f) {
super.iconifyFrame(f);
- if (f instanceof CardInfoWindowDialog) {
+ if (f instanceof MageDesktopIconifySupport) {
+ int needIconWidth = ((MageDesktopIconifySupport) f).getDesktopIconWidth();
JInternalFrame.JDesktopIcon icon = f.getDesktopIcon();
- icon.setBounds(f.getX() + (f.getWidth() - DESKTOP_ICON_WIDTH), f.getY(), DESKTOP_ICON_WIDTH, icon.getHeight());
+ icon.setBounds(f.getX() + (f.getWidth() - needIconWidth), f.getY(), needIconWidth, icon.getHeight());
}
}
@Override
public void deiconifyFrame(JInternalFrame f) {
super.deiconifyFrame(f);
- if (f instanceof CardInfoWindowDialog) {
+ if (f instanceof MageDesktopIconifySupport) {
+ int needIconWidth = ((MageDesktopIconifySupport) f).getDesktopIconWidth();
JInternalFrame.JDesktopIcon icon = f.getDesktopIcon();
- f.setBounds(icon.getX() + (DESKTOP_ICON_WIDTH - f.getWidth()), icon.getY(), f.getWidth(), f.getHeight());
+ f.setBounds(icon.getX() + (needIconWidth - f.getWidth()), icon.getY(), f.getWidth(), f.getHeight());
}
}
diff --git a/Mage.Client/src/main/java/mage/client/dialog/CardHintsHelperDialog.form b/Mage.Client/src/main/java/mage/client/dialog/CardHintsHelperDialog.form
new file mode 100644
index 00000000000..61286ac2b12
--- /dev/null
+++ b/Mage.Client/src/main/java/mage/client/dialog/CardHintsHelperDialog.form
@@ -0,0 +1,160 @@
+
+
+
diff --git a/Mage.Client/src/main/java/mage/client/dialog/CardHintsHelperDialog.java b/Mage.Client/src/main/java/mage/client/dialog/CardHintsHelperDialog.java
new file mode 100644
index 00000000000..3bc2e2fd86c
--- /dev/null
+++ b/Mage.Client/src/main/java/mage/client/dialog/CardHintsHelperDialog.java
@@ -0,0 +1,675 @@
+package mage.client.dialog;
+
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import mage.abilities.hint.HintUtils;
+import mage.client.cards.BigCard;
+import mage.client.chat.ChatPanelBasic;
+import mage.client.components.MageDesktopIconifySupport;
+import mage.client.util.GUISizeHelper;
+import mage.client.util.SettingsManager;
+import mage.client.util.gui.GuiDisplayUtil;
+import mage.util.GameLog;
+import mage.util.RandomUtil;
+import mage.view.CardView;
+import mage.view.EmblemView;
+import mage.view.GameView;
+import mage.view.PlayerView;
+import org.apache.log4j.Logger;
+import org.mage.card.arcane.ManaSymbols;
+import org.mage.plugins.card.utils.impl.ImageManagerImpl;
+
+/**
+ * Game GUI: collect card hints from all visible game's cards
+ *
+ * @author JayDi85
+ */
+public class CardHintsHelperDialog extends MageDialog implements MageDesktopIconifySupport {
+
+ private static final Logger logger = Logger.getLogger(CardHintsHelperDialog.class);
+
+ public static final int GUI_MAX_CARD_HINTS_DIALOGS_PER_GAME = 5; // warning, GUI has a bad performance on too much windows render
+
+ private static final String WINDOW_TITLE = "Card hints helper";
+ private static final String FILTER_ALL = "ALL";
+ private static final String SEARCH_EMPTY_TEXT = "search";
+ private static final String SEARCH_TOOLTIP = "Filter hints by any words in player, card, id or hint";
+ private static final String EMPTY_HINTS_WARNING = "
Game not started or nothing to show";
+
+ private static final int MAX_CARD_IDS_PER_HINT = 3; // for group by hints
+
+ private boolean positioned;
+
+ private GameView lastGameView = null;
+ private final List lastHints = new ArrayList<>();
+
+ private String currentFilter = FILTER_ALL;
+ private CardHintGroupBy currentGroup = CardHintGroupBy.GROUP_BY_HINTS;
+ private String currentSearch = "";
+
+ private static class CardHintInfo {
+ final PlayerView player;
+ final String zone;
+ final CardView card;
+ final List hints;
+ String searchBase;
+ String searchHints;
+
+ public CardHintInfo(PlayerView player, String zone, CardView card) {
+ this.player = player; // can be null for watcher player
+ this.zone = zone;
+ this.card = card;
+ this.hints = new ArrayList<>();
+
+ // additional data
+ if (this.card != null) {
+ List newHints = parseCardHints(this.card.getRules());
+ if (newHints != null) {
+ this.hints.addAll(newHints);
+ }
+ }
+ this.searchBase = (player == null ? "" : player.getName())
+ + "@" + zone
+ + "@" + (card == null ? "" : card.getIdName());
+ this.searchHints = String.join("@", this.hints);
+
+ // searching by lower case
+ this.searchBase = this.searchBase.toLowerCase(Locale.ENGLISH);
+ this.searchHints = this.searchHints.toLowerCase(Locale.ENGLISH);
+ }
+
+ public boolean containsText(List searches) {
+ return searches.stream().anyMatch(s -> this.searchBase.contains(s) || this.searchHints.contains(s));
+ }
+ }
+
+ private enum CardHintGroupBy {
+ GROUP_BY_HINTS("Hints"),
+ GROUP_BY_CARDS("Cards");
+
+ private final String name;
+
+ CardHintGroupBy(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return this.name;
+ }
+ }
+
+ public CardHintsHelperDialog() {
+ this.positioned = false;
+ initComponents();
+
+ // TODO: there are draw artifacts at the last symbol like selection
+ this.hintsView.enableHyperlinksAndCardPopups(); // enable cards popup
+ Color backColor = Color.gray;
+ this.setOpaque(true);
+ this.setBackground(backColor);
+ this.hintsView.setExtBackgroundColor(backColor);
+ this.hintsView.setSelectionColor(Color.gray);
+ this.scrollView.setOpaque(true);
+ this.scrollView.setBackground(backColor);
+ this.scrollView.setViewportBorder(null);
+ this.scrollView.getViewport().setOpaque(true);
+ this.scrollView.getViewport().setBackground(backColor);
+
+ this.setModal(false);
+
+ this.setFrameIcon(new ImageIcon(ImageManagerImpl.instance.getLookedAtImage()));
+ this.setClosable(true);
+ this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+
+ // incremental search support
+ this.search.getDocument().addDocumentListener(new DocumentListener() {
+
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ onSearchStart();
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ onSearchStart();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ onSearchStart();
+ }
+ });
+
+ updateTitle();
+ changeGUISize();
+ }
+
+ private void updateTitle() {
+ // dynamic title
+ List settings = new ArrayList<>();
+
+ // from filter
+ if (!Objects.equals(this.currentFilter, FILTER_ALL)) {
+ settings.add(this.currentFilter);
+ }
+
+ // from group
+ settings.add(this.currentGroup.toString());
+
+ // from search
+ if (this.currentSearch.length() > 0 && !this.currentSearch.equals(SEARCH_EMPTY_TEXT)) {
+ settings.add(this.currentSearch);
+ }
+
+ String newTitle = String.format("%s [%s]", WINDOW_TITLE, String.join(", ", settings));
+ this.setTitle(newTitle);
+ this.setTitelBarToolTip(newTitle);
+ }
+
+ public void cleanUp() {
+ // clean inner objects/components here
+ this.lastGameView = null;
+ this.lastHints.clear();
+ }
+
+ public void setGameData(GameView gameView, UUID gameId, BigCard bigCard) {
+ // one time init on open window
+ this.hintsView.setGameData(gameId, bigCard);
+
+ // prepare gui filter
+ List filters = new ArrayList<>();
+ filters.add(FILTER_ALL);
+ // me
+ gameView.getPlayers()
+ .stream()
+ .filter(PlayerView::getControlled)
+ .map(PlayerView::getName)
+ .findFirst()
+ .ifPresent(filters::add);
+ // other players
+ filters.addAll(gameView.getPlayers()
+ .stream()
+ .filter(p -> !p.getControlled())
+ .map(PlayerView::getName)
+ .collect(Collectors.toList()));
+ this.comboFilterBy.setModel(new DefaultComboBoxModel<>(filters.toArray(new String[0])));
+ this.comboFilterBy.addActionListener(evt -> {
+ this.currentFilter = (String) this.comboFilterBy.getSelectedItem();
+ updateHints();
+ });
+
+ // prepare gui group
+ this.comboGroupBy.setModel(new DefaultComboBoxModel(CardHintGroupBy.values()));
+ this.comboGroupBy.addActionListener(evt -> {
+ this.currentGroup = (CardHintGroupBy) this.comboGroupBy.getSelectedItem();
+ updateHints();
+ });
+ }
+
+ public void loadHints(GameView newGameView) {
+ if (newGameView == null) {
+ return;
+ }
+ if (this.lastGameView != newGameView) {
+ this.lastGameView = newGameView;
+ }
+
+ // collect full hints data
+ this.lastHints.clear();
+
+ // player can be null on watcher
+ PlayerView currentPlayer = this.lastGameView.getPlayers()
+ .stream()
+ .filter(PlayerView::getControlled)
+ .findFirst()
+ .orElse(null);
+
+ // hand
+ this.lastGameView.getHand().values().forEach(card -> {
+ this.lastHints.add(new CardHintInfo(currentPlayer, "hand", card));
+ });
+
+ // stack
+ this.lastGameView.getStack().values().forEach(card -> {
+ this.lastHints.add(new CardHintInfo(currentPlayer, "stack", card));
+ });
+
+ // TODO: add support of revealed, view, player-top and other non CardView?
+
+ // per player
+ for (PlayerView player : this.lastGameView.getPlayers()) {
+ // battlefield
+ player.getBattlefield().values().forEach(card -> {
+ this.lastHints.add(new CardHintInfo(player, "battlefield", card));
+ });
+
+ // commander
+ player.getCommandObjectList().forEach(object -> {
+ // TODO: add support of emblem, dungeon and other non CardView?
+ if (object instanceof CardView) {
+ this.lastHints.add(new CardHintInfo(player, "command", (CardView) object));
+ } else if (object instanceof EmblemView) {
+ //((EmblemView) object).getRules()
+ }
+ });
+
+ // graveyard
+ player.getGraveyard().values().forEach(card -> {
+ this.lastHints.add(new CardHintInfo(player, "graveyard", card));
+ });
+
+ // exile
+ player.getExile().values().forEach(card -> {
+ this.lastHints.add(new CardHintInfo(player, "exile", card));
+ });
+ }
+
+ // keep cards with hints only
+ this.lastHints.removeIf(info -> !info.card.getRules().contains(HintUtils.HINT_START_MARK));
+
+ updateHints();
+ }
+
+ public void updateHints() {
+ // use already prepared data from loadHints
+
+ List filteredCards = new ArrayList<>(this.lastHints);
+
+ // apply player filter
+ if (!this.currentFilter.equals(FILTER_ALL)) {
+ filteredCards.removeIf(info -> info.player == null || !info.player.getName().equals(this.currentFilter));
+ }
+
+ // apply search filter
+ if (!this.currentSearch.isEmpty()) {
+ List needSearches = new ArrayList<>(Arrays.asList(this.currentSearch.toLowerCase(Locale.ENGLISH).trim().split(" ")));
+ filteredCards.removeIf(info -> !(info.containsText(needSearches)));
+ }
+
+ // apply group type
+ List renderData = new ArrayList<>();
+ switch (this.currentGroup) {
+ // data must be sorted and grouped already in prepared code
+
+ default:
+ case GROUP_BY_HINTS: {
+ // player -> hint + cards
+ Map> groupsByZone = prepareGroupByPlayerAndZones(filteredCards, true, false);
+ groupsByZone.forEach((group, groupCards) -> {
+ if (groupCards.isEmpty()) {
+ return;
+ }
+ if (!renderData.isEmpty()) {
+ renderData.add("
");
+ }
+
+ // header: player
+ CardHintInfo sampleData = groupCards.get(0);
+ renderData.add(String.format("%s:",
+ GameLog.getColoredPlayerName(sampleData.player == null ? "watcher" : sampleData.player.getName())
+ ));
+
+ // data: unique hints
+ Map> groupByHints = prepareGroupByHints(groupCards);
+
+ groupByHints.forEach((groupByHint, groupByHintCards) -> {
+ if (groupByHintCards.isEmpty()) {
+ return;
+ }
+
+ // hint
+ String renderLine = groupByHint;
+
+ // data: cards list like [123], [456], [789] and 2 other
+ String cardNames = groupByHintCards
+ .stream()
+ .limit(MAX_CARD_IDS_PER_HINT)
+ .map(info -> {
+ // workable card hints
+ return GameLog.getColoredObjectIdName(
+ info.card.getColor(),
+ info.card.getId(),
+ String.format("[%s]", info.card.getId().toString().substring(0, 3)),
+ "",
+ info.card.getName()
+ );
+ })
+ .collect(Collectors.joining(", "));
+ if (groupByHintCards.size() > MAX_CARD_IDS_PER_HINT) {
+ cardNames += String.format(" and %d other", groupByHintCards.size() - MAX_CARD_IDS_PER_HINT);
+ }
+ renderLine += " (" + cardNames + ")";
+
+ renderData.add(renderLine);
+ });
+ });
+ break;
+ }
+
+ case GROUP_BY_CARDS: {
+ // player + zone -> card + hint
+ Map> groupsByZone = prepareGroupByPlayerAndZones(filteredCards, true, true);
+ groupsByZone.forEach((group, groupCards) -> {
+ if (groupCards.isEmpty()) {
+ return;
+ }
+ if (!renderData.isEmpty()) {
+ renderData.add("
");
+ }
+
+ // header: player/zone
+ CardHintInfo sampleData = groupCards.get(0);
+ renderData.add(String.format("%s - %s:",
+ GameLog.getColoredPlayerName(sampleData.player == null ? "watcher" : sampleData.player.getName()),
+ sampleData.zone
+ ));
+
+ // data: cards list
+ groupCards.forEach(info -> {
+ List hints = parseCardHints(info.card.getRules());
+ if (hints != null) {
+ String cardName = GameLog.getColoredObjectIdName(
+ info.card.getColor(),
+ info.card.getId(),
+ info.card.getName(),
+ String.format("[%s]", info.card.getId().toString().substring(0, 3)),
+ null
+ );
+ renderData.addAll(hints.stream().map(s -> cardName + ": " + s).collect(Collectors.toList()));
+ }
+ });
+ });
+ break;
+ }
+ }
+
+ // empty
+ if (renderData.isEmpty()) {
+ renderData.add(EMPTY_HINTS_WARNING);
+ }
+
+ // final render
+ String renderFinal = String.join("
", renderData);
+ // keep scrolls position between updates
+ int oldPos = this.scrollView.getVerticalScrollBar().getValue();
+ this.hintsView.setText("" + ManaSymbols.replaceSymbolsWithHTML(renderFinal, ManaSymbols.Type.CHAT));
+ SwingUtilities.invokeLater(() -> {
+ this.scrollView.getVerticalScrollBar().setValue(oldPos);
+ });
+
+ updateTitle();
+ showAndPositionWindow();
+ }
+
+ private Map> prepareGroupByPlayerAndZones(
+ List currentHints,
+ boolean groupByPlayer,
+ boolean groupByZone
+ ) {
+ if (!groupByPlayer && !groupByZone) {
+ throw new IllegalArgumentException("Wrong code usage: must use minimum one group param");
+ }
+
+ Map> res = new LinkedHashMap<>();
+ if (currentHints.isEmpty()) {
+ return res;
+ }
+
+ String lastGroupName = null;
+ List lastGroupCards = null;
+ for (CardHintInfo info : currentHints) {
+ String currentGroupName = "";
+ if (groupByPlayer) {
+ currentGroupName += "@" + (info.player == null ? "null" : info.player.getName());
+ }
+ if (groupByZone) {
+ currentGroupName += "@" + info.zone;
+ }
+
+ if (lastGroupCards == null || !lastGroupName.equals(currentGroupName)) {
+ lastGroupName = currentGroupName;
+ lastGroupCards = new ArrayList<>();
+ res.put(currentGroupName, lastGroupCards);
+ }
+ lastGroupCards.add(info);
+ }
+
+ // sort cards by card name inside
+ res.forEach((zone, infos) -> {
+ infos.sort(Comparator.comparing(o -> o.card.getName()));
+ });
+
+ return res;
+ }
+
+ private Map> prepareGroupByHints(List currentHints) {
+ Map> res = new LinkedHashMap<>();
+ if (currentHints.isEmpty()) {
+ return res;
+ }
+
+ // unique and sorted hints
+ List allPossibleHints = currentHints
+ .stream()
+ .map(info -> parseCardHints(info.card.getRules()))
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .distinct()
+ .sorted()
+ .collect(Collectors.toList());
+
+ allPossibleHints.forEach(currentHintName -> {
+ List cards = res.getOrDefault(currentHintName, null);
+ if (cards == null) {
+ cards = new ArrayList<>();
+ res.put(currentHintName, cards);
+ }
+
+ for (CardHintInfo info : currentHints) {
+ if (info.card.getRules().contains(currentHintName)) {
+ cards.add(info);
+ }
+ }
+ });
+
+ // sort by card id (cause it show id instead name)
+ res.forEach((hint, cards) -> {
+ cards.sort(Comparator.comparing(o -> o.card.getId()));
+ });
+
+ return res;
+ }
+
+ private static List parseCardHints(List rules) {
+ if (rules.isEmpty() || !rules.contains(HintUtils.HINT_START_MARK)) {
+ return null;
+ }
+ List hints = new ArrayList<>();
+ boolean started = false;
+ for (String rule : rules) {
+ if (rule.equals(HintUtils.HINT_START_MARK)) {
+ started = true;
+ continue;
+ }
+ if (started) {
+ hints.add(rule);
+ }
+ }
+ return hints;
+ }
+
+ @Override
+ public void changeGUISize() {
+ setGUISize(GUISizeHelper.chatFont);
+ this.validate();
+ this.repaint();
+ }
+
+ private void setGUISize(Font font) {
+ this.hintsView.setFont(font);
+ this.scrollView.setFont(font);
+ this.scrollView.getVerticalScrollBar().setPreferredSize(new Dimension(GUISizeHelper.scrollBarSize, 0));
+ this.scrollView.getHorizontalScrollBar().setPreferredSize(new Dimension(0, GUISizeHelper.scrollBarSize));
+ }
+
+ @Override
+ public void show() {
+ super.show();
+
+ // auto-position on first usage
+ if (positioned) {
+ showAndPositionWindow();
+ }
+ }
+
+ private void showAndPositionWindow() {
+ SwingUtilities.invokeLater(() -> {
+ int width = CardHintsHelperDialog.this.getWidth();
+ int height = CardHintsHelperDialog.this.getHeight();
+ if (width > 0 && height > 0) {
+ Point centered = SettingsManager.instance.getComponentPosition(width, height);
+ if (!positioned) {
+ // starting position
+ // little randomize to see multiple opened windows
+ int xPos = centered.x / 2 + RandomUtil.nextInt(50);
+ int yPos = centered.y / 2 + RandomUtil.nextInt(50);
+ CardHintsHelperDialog.this.setLocation(xPos, yPos);
+ show();
+ positioned = true;
+ }
+ GuiDisplayUtil.keepComponentInsideFrame(centered.x, centered.y, CardHintsHelperDialog.this);
+ }
+
+ // workaround to fix draw artifacts
+ //CardHintsHelperDialog.this.validate();
+ //CardHintsHelperDialog.this.repaint();
+ });
+ }
+
+ private void onSearchFocused() {
+ // fast select
+ if (SEARCH_EMPTY_TEXT.equals(search.getText())) {
+ search.setText("");
+ } else if (search.getText().length() > 0) {
+ search.selectAll();
+ }
+ }
+
+ private void onSearchStart() {
+ String newSearch = SEARCH_EMPTY_TEXT.equals(search.getText()) ? "" : search.getText();
+ if (!this.currentSearch.equals(newSearch)) {
+ this.currentSearch = newSearch;
+ updateHints();
+ }
+ }
+
+ private void onSearchClear() {
+ this.search.setText(SEARCH_EMPTY_TEXT);
+ this.currentSearch = "";
+ updateHints();
+ }
+
+ /**
+ * This method is called from within the constructor to initialize the form.
+ * WARNING: Do NOT modify this code. The content of this method is always
+ * regenerated by the Form Editor.
+ */
+ @SuppressWarnings("unchecked")
+ // //GEN-BEGIN:initComponents
+ private void initComponents() {
+
+ scrollView = new javax.swing.JScrollPane();
+ hintsView = new mage.client.components.ColorPane();
+ commands = new javax.swing.JPanel();
+ comboFilterBy = new javax.swing.JComboBox<>();
+ comboGroupBy = new javax.swing.JComboBox<>();
+ searchPanel = new javax.swing.JPanel();
+ search = new javax.swing.JTextField();
+ searchClear = new javax.swing.JButton();
+
+ setClosable(true);
+ setIconifiable(true);
+ setResizable(true);
+ setTitle("Card hints helper");
+ setMinimumSize(new java.awt.Dimension(200, 100));
+ setPreferredSize(new java.awt.Dimension(400, 300));
+ setTitelBarToolTip("");
+ getContentPane().setLayout(new java.awt.BorderLayout());
+
+ scrollView.setBorder(null);
+ scrollView.setOpaque(true);
+
+ hintsView.setEditable(false);
+ hintsView.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5));
+ hintsView.setFont(new java.awt.Font("Arial", 0, 14)); // NOI18N
+ hintsView.setText(" test text");
+ hintsView.setFocusable(false);
+ hintsView.setOpaque(true);
+ scrollView.setViewportView(hintsView);
+
+ getContentPane().add(scrollView, java.awt.BorderLayout.CENTER);
+
+ commands.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5));
+ commands.setLayout(new java.awt.GridLayout(1, 3, 5, 5));
+
+ comboFilterBy.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
+ commands.add(comboFilterBy);
+
+ comboGroupBy.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
+ commands.add(comboGroupBy);
+
+ searchPanel.setLayout(new java.awt.BorderLayout());
+
+ search.setText(SEARCH_EMPTY_TEXT);
+ search.setToolTipText(SEARCH_TOOLTIP);
+ search.addFocusListener(new java.awt.event.FocusAdapter() {
+ public void focusGained(java.awt.event.FocusEvent evt) {
+ searchFocusGained(evt);
+ }
+ });
+ searchPanel.add(search, java.awt.BorderLayout.CENTER);
+
+ searchClear.setIcon(new javax.swing.ImageIcon(getClass().getResource("/buttons/sideboard_out.png"))); // NOI18N
+ searchClear.setToolTipText("Clear search field");
+ searchClear.setPreferredSize(new java.awt.Dimension(23, 23));
+ searchClear.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ searchClearActionPerformed(evt);
+ }
+ });
+ searchPanel.add(searchClear, java.awt.BorderLayout.EAST);
+
+ commands.add(searchPanel);
+
+ getContentPane().add(commands, java.awt.BorderLayout.NORTH);
+
+ pack();
+ }// //GEN-END:initComponents
+
+ private void searchFocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_searchFocusGained
+ onSearchFocused();
+ }//GEN-LAST:event_searchFocusGained
+
+ private void searchClearActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchClearActionPerformed
+ onSearchClear();
+ }//GEN-LAST:event_searchClearActionPerformed
+
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JComboBox comboFilterBy;
+ private javax.swing.JComboBox comboGroupBy;
+ private javax.swing.JPanel commands;
+ private mage.client.components.ColorPane hintsView;
+ private javax.swing.JScrollPane scrollView;
+ private javax.swing.JTextField search;
+ private javax.swing.JButton searchClear;
+ private javax.swing.JPanel searchPanel;
+ // End of variables declaration//GEN-END:variables
+
+}
diff --git a/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java b/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java
index f4d178ea4fc..e5427247754 100644
--- a/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java
+++ b/Mage.Client/src/main/java/mage/client/dialog/CardInfoWindowDialog.java
@@ -12,11 +12,13 @@ import javax.swing.plaf.basic.BasicInternalFrameUI;
import mage.cards.MageCard;
import mage.client.cards.BigCard;
+import mage.client.components.MageDesktopIconifySupport;
import mage.client.util.GUISizeHelper;
import mage.client.util.ImageHelper;
import mage.client.util.SettingsManager;
import mage.client.util.gui.GuiDisplayUtil;
import mage.constants.CardType;
+import mage.util.RandomUtil;
import mage.view.CardView;
import mage.view.CardsView;
import mage.view.ExileView;
@@ -29,7 +31,7 @@ import org.mage.plugins.card.utils.impl.ImageManagerImpl;
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/
-public class CardInfoWindowDialog extends MageDialog {
+public class CardInfoWindowDialog extends MageDialog implements MageDesktopIconifySupport {
private static final Logger LOGGER = Logger.getLogger(CardInfoWindowDialog.class);
@@ -48,22 +50,6 @@ public class CardInfoWindowDialog extends MageDialog {
this.positioned = false;
initComponents();
- // ENABLE a minimizing window on double clicks
- BasicInternalFrameUI ui = (BasicInternalFrameUI) this.getUI();
- ui.getNorthPane().addMouseListener(new MouseAdapter() {
- @Override
- public void mouseClicked(MouseEvent e) {
- if ((e.getClickCount() & 1) == 0 && (e.getClickCount() > 0) && !e.isConsumed()) { // double clicks and repeated double clicks
- e.consume();
- try {
- CardInfoWindowDialog.this.setIcon(!CardInfoWindowDialog.this.isIcon());
- } catch (PropertyVetoException exp) {
- // ignore read only
- }
- }
- }
- });
-
this.setModal(false);
switch (this.showType) {
case LOOKED_AT:
@@ -192,6 +178,7 @@ public class CardInfoWindowDialog extends MageDialog {
@Override
public void show() {
+ // hide empty exile windows
if (showType == ShowType.EXILE) {
if (cards == null || cards.getNumberOfCards() == 0) {
return;
@@ -199,7 +186,9 @@ public class CardInfoWindowDialog extends MageDialog {
}
super.show();
- if (positioned) { // check if in frame rectangle
+
+ // auto-position on first usage
+ if (positioned) {
showAndPositionWindow();
}
}
@@ -211,8 +200,10 @@ public class CardInfoWindowDialog extends MageDialog {
if (width > 0 && height > 0) {
Point centered = SettingsManager.instance.getComponentPosition(width, height);
if (!positioned) {
- int xPos = centered.x / 2;
- int yPos = centered.y / 2;
+ // starting position
+ // little randomize to see multiple opened windows
+ int xPos = centered.x / 2 + RandomUtil.nextInt(50);
+ int yPos = centered.y / 2 + RandomUtil.nextInt(50);
CardInfoWindowDialog.this.setLocation(xPos, yPos);
show();
positioned = true;
diff --git a/Mage.Client/src/main/java/mage/client/dialog/MageDialog.java b/Mage.Client/src/main/java/mage/client/dialog/MageDialog.java
index 81642009718..d31be34d434 100644
--- a/Mage.Client/src/main/java/mage/client/dialog/MageDialog.java
+++ b/Mage.Client/src/main/java/mage/client/dialog/MageDialog.java
@@ -1,13 +1,16 @@
package mage.client.dialog;
import mage.client.MageFrame;
+import mage.client.components.MageDesktopIconifySupport;
import mage.client.util.SettingsManager;
import mage.client.util.gui.GuiDisplayUtil;
import org.apache.log4j.Logger;
import javax.swing.*;
+import javax.swing.plaf.basic.BasicInternalFrameUI;
import java.awt.*;
import java.awt.event.InvocationEvent;
+import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyVetoException;
import java.lang.reflect.InvocationTargetException;
@@ -26,6 +29,25 @@ public class MageDialog extends javax.swing.JInternalFrame {
*/
public MageDialog() {
initComponents();
+
+ // enable a minimizing window on double clicks
+ if (this instanceof MageDesktopIconifySupport) {
+ BasicInternalFrameUI ui = (BasicInternalFrameUI) this.getUI();
+ ui.getNorthPane().addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ // double clicks and repeated double clicks
+ if ((e.getClickCount() & 1) == 0 && (e.getClickCount() > 0) && !e.isConsumed()) {
+ e.consume();
+ try {
+ MageDialog.this.setIcon(!MageDialog.this.isIcon());
+ } catch (PropertyVetoException exp) {
+ // ignore read only
+ }
+ }
+ }
+ });
+ }
}
public void changeGUISize() {
diff --git a/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java b/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java
index 775d18e9bbf..2419c5101b5 100644
--- a/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java
+++ b/Mage.Client/src/main/java/mage/client/dialog/ShowCardsDialog.java
@@ -9,6 +9,7 @@
import mage.client.util.SettingsManager;
import mage.client.util.gui.GuiDisplayUtil;
import mage.game.events.PlayerQueryEvent.QueryType;
+ import mage.util.RandomUtil;
import mage.view.CardsView;
import org.mage.card.arcane.CardPanel;
@@ -38,7 +39,6 @@
initComponents();
this.setModal(false);
-
}
public void cleanUp() {
@@ -58,9 +58,40 @@
}
private void setGUISize() {
-
+ // nothing to change (all components in cardArea)
}
+ @Override
+ public void show() {
+ super.show();
+
+ // auto-position on first usage
+ if (positioned) {
+ showAndPositionWindow();
+ }
+ }
+
+ private void showAndPositionWindow() {
+ SwingUtilities.invokeLater(() -> {
+ int width = ShowCardsDialog.this.getWidth();
+ int height = ShowCardsDialog.this.getHeight();
+ if (width > 0 && height > 0) {
+ Point centered = SettingsManager.instance.getComponentPosition(width, height);
+ if (!positioned) {
+ // starting position
+ // little randomize to see multiple opened windows
+ int xPos = centered.x / 2 + RandomUtil.nextInt(50);
+ int yPos = centered.y / 2 + RandomUtil.nextInt(50);
+ ShowCardsDialog.this.setLocation(xPos, yPos);
+ show();
+ positioned = true;
+ }
+ GuiDisplayUtil.keepComponentInsideFrame(centered.x, centered.y, ShowCardsDialog.this);
+ }
+ });
+ }
+
+
public void loadCards(String name, CardsView showCards, BigCard bigCard,
UUID gameId, boolean modal, Map options,
JPopupMenu popupMenu, Listener eventListener) {
@@ -101,20 +132,6 @@
} else {
MageFrame.getDesktop().add(this, JLayeredPane.PALETTE_LAYER);
}
-
- SwingUtilities.invokeLater(() -> {
- if (!positioned) {
- int width = ShowCardsDialog.this.getWidth();
- int height = ShowCardsDialog.this.getHeight();
- if (width > 0 && height > 0) {
- Point centered = SettingsManager.instance.getComponentPosition(width, height);
- ShowCardsDialog.this.setLocation(centered.x, centered.y);
- positioned = true;
- GuiDisplayUtil.keepComponentInsideScreen(centered.x, centered.y, ShowCardsDialog.this);
- }
- }
- ShowCardsDialog.this.setVisible(true);
- });
}
private void initComponents() {
diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java
index eb809d80c14..d74c8c8f79d 100644
--- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java
+++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java
@@ -86,6 +86,8 @@ public final class GamePanel extends javax.swing.JPanel {
private final Map sideboardWindows = new HashMap<>();
private final ArrayList pickTarget = new ArrayList<>();
private final ArrayList pickPile = new ArrayList<>();
+ private final Map cardHintsWindows = new LinkedHashMap<>();
+
private UUID gameId;
private UUID playerId; // playerId of the player
GamePane gamePane;
@@ -275,6 +277,10 @@ public final class GamePanel extends javax.swing.JPanel {
windowDialog.cleanUp();
windowDialog.removeDialog();
}
+ for (CardHintsHelperDialog windowDialog : cardHintsWindows.values()) {
+ windowDialog.cleanUp();
+ windowDialog.removeDialog();
+ }
clearPickDialogs();
@@ -350,6 +356,9 @@ public final class GamePanel extends javax.swing.JPanel {
for (CardInfoWindowDialog windowDialog : sideboardWindows.values()) {
windowDialog.changeGUISize();
}
+ for (CardHintsHelperDialog windowDialog : cardHintsWindows.values()) {
+ windowDialog.changeGUISize();
+ }
for (ShowCardsDialog windowDialog : pickTarget) {
windowDialog.changeGUISize();
}
@@ -886,16 +895,25 @@ public final class GamePanel extends javax.swing.JPanel {
GameManager.instance.setStackSize(lastGameData.game.getStack().size());
displayStack(lastGameData.game, bigCard, feedbackPanel, gameId);
+ // auto-show exile views
for (ExileView exile : lastGameData.game.getExile()) {
- if (!exiles.containsKey(exile.getId())) {
- CardInfoWindowDialog newExile = new CardInfoWindowDialog(ShowType.EXILE, exile.getName());
- exiles.put(exile.getId(), newExile);
- MageFrame.getDesktop().add(newExile, JLayeredPane.PALETTE_LAYER);
- newExile.show();
+ CardInfoWindowDialog exileWindow = exiles.getOrDefault(exile.getId(), null);
+ if (exileWindow == null) {
+ exileWindow = new CardInfoWindowDialog(ShowType.EXILE, exile.getName());
+ exiles.put(exile.getId(), exileWindow);
+ MageFrame.getDesktop().add(exileWindow, JLayeredPane.PALETTE_LAYER);
+ exileWindow.show();
}
- exiles.get(exile.getId()).loadCards(exile, bigCard, gameId);
+ exileWindow.loadCards(exile, bigCard, gameId);
}
+ // update open or remove closed card hints windows
+ clearClosedCardHintsWindows();
+ cardHintsWindows.forEach((s, windowDialog) -> {
+ // TODO: optimize for multiple windows (prepare data here and send it for filters/groups)
+ windowDialog.loadHints(lastGameData.game);
+ });
+
// reveal and look at dialogs can unattached, so windows opened by game doesn't have it
showRevealed(lastGameData.game);
showLookedAt(lastGameData.game);
@@ -1197,24 +1215,28 @@ public final class GamePanel extends javax.swing.JPanel {
// Called if the game frame is deactivated because the tabled the deck editor or other frames go to foreground
public void deactivated() {
// hide the non modal windows (because otherwise they are shown on top of the new active pane)
+ // TODO: is it need to hide other dialogs like graveyards (CardsView)?
for (CardInfoWindowDialog windowDialog : exiles.values()) {
windowDialog.hideDialog();
}
- for (CardInfoWindowDialog windowDialog : graveyardWindows.values()) {
- windowDialog.hideDialog();
- }
for (CardInfoWindowDialog windowDialog : revealed.values()) {
windowDialog.hideDialog();
}
for (CardInfoWindowDialog windowDialog : lookedAt.values()) {
windowDialog.hideDialog();
}
+ for (CardInfoWindowDialog windowDialog : graveyardWindows.values()) {
+ windowDialog.hideDialog();
+ }
for (CardInfoWindowDialog windowDialog : companion.values()) {
windowDialog.hideDialog();
}
for (CardInfoWindowDialog windowDialog : sideboardWindows.values()) {
windowDialog.hideDialog();
}
+ for (CardHintsHelperDialog windowDialog : cardHintsWindows.values()) {
+ windowDialog.hideDialog();
+ }
}
// Called if the game frame comes to front again
@@ -1223,21 +1245,24 @@ public final class GamePanel extends javax.swing.JPanel {
for (CardInfoWindowDialog windowDialog : exiles.values()) {
windowDialog.show();
}
- for (CardInfoWindowDialog windowDialog : graveyardWindows.values()) {
- windowDialog.show();
- }
for (CardInfoWindowDialog windowDialog : revealed.values()) {
windowDialog.show();
}
for (CardInfoWindowDialog windowDialog : lookedAt.values()) {
windowDialog.show();
}
+ for (CardInfoWindowDialog windowDialog : graveyardWindows.values()) {
+ windowDialog.show();
+ }
for (CardInfoWindowDialog windowDialog : companion.values()) {
windowDialog.show();
}
for (CardInfoWindowDialog windowDialog : sideboardWindows.values()) {
windowDialog.show();
}
+ for (CardHintsHelperDialog windowDialog : cardHintsWindows.values()) {
+ windowDialog.show();
+ }
}
public void openGraveyardWindow(String playerName) {
@@ -1257,6 +1282,29 @@ public final class GamePanel extends javax.swing.JPanel {
newGraveyard.loadCards(graveyards.get(playerName), bigCard, gameId, false);
}
+ private void clearClosedCardHintsWindows() {
+ cardHintsWindows.entrySet().removeIf(entry -> entry.getValue().isClosed());
+ }
+
+ public void openCardHintsWindow(String code) {
+ // clear closed
+ clearClosedCardHintsWindows();
+
+ // too many dialogs can cause bad GUI performance, so limit it
+ if (cardHintsWindows.size() > CardHintsHelperDialog.GUI_MAX_CARD_HINTS_DIALOGS_PER_GAME) {
+ // show last one instead
+ cardHintsWindows.values().stream().reduce((a, b) -> b).ifPresent(CardHintsHelperDialog::show);
+ return;
+ }
+
+ // open new
+ CardHintsHelperDialog newDialog = new CardHintsHelperDialog();
+ newDialog.setGameData(this.lastGameData.game, this.gameId, this.bigCard);
+ cardHintsWindows.put(code + UUID.randomUUID(), newDialog);
+ MageFrame.getDesktop().add(newDialog, JLayeredPane.PALETTE_LAYER);
+ newDialog.loadHints(lastGameData.game);
+ }
+
public void openSideboardWindow(UUID playerId) {
if (lastGameData == null) {
return;
diff --git a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java
index abf74ca67e5..4a9f4a411f4 100644
--- a/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java
+++ b/Mage.Client/src/main/java/mage/client/game/PlayAreaPanel.java
@@ -33,6 +33,7 @@ public class PlayAreaPanel extends javax.swing.JPanel {
private final JPopupMenu popupMenu;
private UUID playerId;
private UUID gameId;
+ private boolean isMe = false;
private boolean smallMode = false;
private boolean playingMode = true;
private final GamePanel gamePanel;
@@ -46,6 +47,7 @@ public class PlayAreaPanel extends javax.swing.JPanel {
public static final int PANEL_HEIGHT = 263;
public static final int PANEL_HEIGHT_SMALL = 210;
+ private static final int PANEL_HEIGHT_EXTRA_FOR_ME = 25;
/**
* Creates new form PlayAreaPanel
@@ -515,6 +517,7 @@ public class PlayAreaPanel extends javax.swing.JPanel {
this.battlefieldPanel.init(gameId, bigCard);
this.gameId = gameId;
this.playerId = player.getPlayerId();
+ this.isMe = player.getControlled();
this.btnCheat.setVisible(SessionHandler.isTestMode());
}
@@ -562,14 +565,15 @@ public class PlayAreaPanel extends javax.swing.JPanel {
public void sizePlayer(boolean smallMode) {
this.playerPanel.sizePlayerPanel(smallMode);
this.smallMode = smallMode;
+ int extraForMe = this.isMe ? PANEL_HEIGHT_EXTRA_FOR_ME : 0;
if (smallMode) {
- this.playerPanel.setPreferredSize(new Dimension(92, PANEL_HEIGHT_SMALL));
+ this.playerPanel.setPreferredSize(new Dimension(92, PANEL_HEIGHT_SMALL + extraForMe));
//this.jScrollPane1.setPreferredSize(new Dimension(160, 160));
- this.battlefieldPanel.setPreferredSize(new Dimension(160, PANEL_HEIGHT_SMALL));
+ this.battlefieldPanel.setPreferredSize(new Dimension(160, PANEL_HEIGHT_SMALL + extraForMe));
} else {
- this.playerPanel.setPreferredSize(new Dimension(92, PANEL_HEIGHT));
+ this.playerPanel.setPreferredSize(new Dimension(92, PANEL_HEIGHT + extraForMe));
//this.jScrollPane1.setPreferredSize(new Dimension(160, 212));
- this.battlefieldPanel.setPreferredSize(new Dimension(160, PANEL_HEIGHT));
+ this.battlefieldPanel.setPreferredSize(new Dimension(160, PANEL_HEIGHT + extraForMe));
}
}
diff --git a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java
index 24e95333b4d..7dfd82e6c48 100644
--- a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java
+++ b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java
@@ -46,6 +46,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
private UUID playerId;
private UUID gameId;
private PlayerView player;
+ private boolean isMe;
private BigCard bigCard;
@@ -53,11 +54,13 @@ public class PlayerPanelExt extends javax.swing.JPanel {
private static final int PANEL_WIDTH = 94;
private static final int PANEL_HEIGHT = 262;
- private static final int PANEL_HEIGHT_SMALL = 242;
+ private static final int PANEL_HEIGHT_SMALL = 210;
+ private static final int PANEL_HEIGHT_EXTRA_FOR_ME = 25;
private static final int MANA_LABEL_SIZE_HORIZONTAL = 20;
private static final Border GREEN_BORDER = new LineBorder(Color.green, 3);
private static final Border RED_BORDER = new LineBorder(Color.red, 2);
+ private static final Border YELLOW_BORDER = new LineBorder(Color.yellow, 3);
private static final Border EMPTY_BORDER = BorderFactory.createEmptyBorder(0, 0, 0, 0);
private final Color inactiveBackgroundColor;
private final Color activeBackgroundColor;
@@ -92,8 +95,10 @@ public class PlayerPanelExt extends javax.swing.JPanel {
this.gameId = gameId;
this.playerId = playerId;
this.bigCard = bigCard;
- cheat.setVisible(SessionHandler.isTestMode() && controlled);
+ this.isMe = controlled;
+ cheat.setVisible(SessionHandler.isTestMode() && this.isMe);
cheat.setFocusable(false);
+ toolHintsHelper.setVisible(this.isMe);
flagName = null;
if (priorityTime > 0) {
long delay = 1000L;
@@ -355,6 +360,12 @@ public class PlayerPanelExt extends javax.swing.JPanel {
}
}
+ // possible targeting
+ if (possibleTargets != null && possibleTargets.contains(this.playerId)) {
+ this.avatar.setBorder(YELLOW_BORDER);
+ this.btnPlayer.setBorder(YELLOW_BORDER);
+ }
+
update(player.getManaPool());
}
@@ -568,6 +579,13 @@ public class PlayerPanelExt extends javax.swing.JPanel {
cheat.setToolTipText("Cheat button");
cheat.addActionListener(e -> btnCheatActionPerformed(e));
+ // Tools button
+ r = new Rectangle(75, 21);
+ toolHintsHelper = new JButton();
+ toolHintsHelper.setText("hints");
+ toolHintsHelper.setToolTipText("Open new card hints helper window");
+ toolHintsHelper.addActionListener(e -> btnToolHintsHelperActionPerformed(e));
+
zonesPanel = new JPanel();
zonesPanel.setPreferredSize(new Dimension(100, 60));
zonesPanel.setSize(100, 60);
@@ -591,6 +609,9 @@ public class PlayerPanelExt extends javax.swing.JPanel {
cheat.setBounds(40, 2, 25, 21);
zonesPanel.add(cheat);
+ toolHintsHelper.setBounds(3, 2 + 21 + 2, 75, 21);
+ zonesPanel.add(toolHintsHelper);
+
energyExperiencePanel = new JPanel();
energyExperiencePanel.setPreferredSize(new Dimension(100, 20));
energyExperiencePanel.setSize(100, 20);
@@ -619,6 +640,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
btnPlayer.setText("Player");
btnPlayer.setVisible(false);
btnPlayer.setToolTipText("Player");
+ btnPlayer.setPreferredSize(new Dimension(20, 40));
btnPlayer.addActionListener(e -> SessionHandler.sendPlayerUUID(gameId, playerId));
// Add mana symbols
@@ -808,7 +830,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
.addGap(6)
.addComponent(avatar, GroupLayout.PREFERRED_SIZE, 80, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(ComponentPlacement.RELATED)
- .addComponent(btnPlayer)
+ .addComponent(btnPlayer, GroupLayout.PREFERRED_SIZE, 30, GroupLayout.PREFERRED_SIZE)
.addComponent(timerLabel)
.addGap(2)
// Life & Hand
@@ -912,18 +934,19 @@ public class PlayerPanelExt extends javax.swing.JPanel {
}// //GEN-END:initComponents
protected void sizePlayerPanel(boolean smallMode) {
+ int extraForMe = this.isMe ? PANEL_HEIGHT_EXTRA_FOR_ME : 0;
if (smallMode) {
avatar.setVisible(false);
btnPlayer.setVisible(true);
timerLabel.setVisible(true);
- panelBackground.setPreferredSize(new Dimension(PANEL_WIDTH - 2, PANEL_HEIGHT_SMALL));
- panelBackground.setBounds(0, 0, PANEL_WIDTH - 2, PANEL_HEIGHT_SMALL);
+ panelBackground.setPreferredSize(new Dimension(PANEL_WIDTH - 2, PANEL_HEIGHT_SMALL + extraForMe));
+ panelBackground.setBounds(0, 0, PANEL_WIDTH - 2, PANEL_HEIGHT_SMALL + extraForMe);
} else {
avatar.setVisible(true);
btnPlayer.setVisible(false);
timerLabel.setVisible(false);
- panelBackground.setPreferredSize(new Dimension(PANEL_WIDTH - 2, PANEL_HEIGHT));
- panelBackground.setBounds(0, 0, PANEL_WIDTH - 2, PANEL_HEIGHT);
+ panelBackground.setPreferredSize(new Dimension(PANEL_WIDTH - 2, PANEL_HEIGHT + extraForMe));
+ panelBackground.setBounds(0, 0, PANEL_WIDTH - 2, PANEL_HEIGHT + extraForMe);
}
}
@@ -952,6 +975,10 @@ public class PlayerPanelExt extends javax.swing.JPanel {
SessionHandler.cheat(gameId, playerId, deckImporter.importDeck("cheat.dck", false));
}
+ private void btnToolHintsHelperActionPerformed(java.awt.event.ActionEvent evt) {
+ MageFrame.getGame(gameId).openCardHintsWindow("main");
+ }
+
public PlayerView getPlayer() {
return player;
}
@@ -984,6 +1011,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
private HoverButton grave;
private HoverButton library;
private JButton cheat;
+ private JButton toolHintsHelper;
private MageRoundPane panelBackground;
private JLabel timerLabel;
diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java
index 09cf8e92cd4..d6b49557a7a 100644
--- a/Mage.Common/src/main/java/mage/view/CardView.java
+++ b/Mage.Common/src/main/java/mage/view/CardView.java
@@ -1386,4 +1386,8 @@ public class CardView extends SimpleCardView {
public boolean showPT() {
return this.isCreature() || this.getSubTypes().contains(SubType.VEHICLE);
}
+
+ public String getIdName() {
+ return getName() + " [" + getId().toString().substring(0, 3) + ']';
+ }
}
diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/CardHintsTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/CardHintsTest.java
new file mode 100644
index 00000000000..2664737b520
--- /dev/null
+++ b/Mage.Tests/src/test/java/org/mage/test/utils/CardHintsTest.java
@@ -0,0 +1,129 @@
+package org.mage.test.utils;
+
+import mage.MageObject;
+import mage.abilities.keyword.FlyingAbility;
+import mage.constants.CommanderCardType;
+import mage.constants.PhaseStep;
+import mage.constants.Zone;
+import mage.util.GameLog;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mage.test.serverside.base.CardTestCommanderDuelBase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CardHintsTest extends CardTestCommanderDuelBase {
+
+ // html logs/names used all around xmage (game logs, chats, choose dialogs, etc)
+ // usage steps:
+ // * server side: find game object for logs;
+ // * server side: prepare html compatible log (name [id] + color + additional info)
+ // * client side: inject additional elements for popup support (e.g. "a" with "href")
+ // * client side: process mouse move over a href and show object data like a card popup
+
+ private void assertObjectHtmlLog(String originalLog, String needVisibleColorPart, String needVisibleNormalPart, String needId) {
+ String needVisibleFull = needVisibleColorPart;
+ if (!needVisibleNormalPart.isEmpty()) {
+ needVisibleFull += !needVisibleFull.isEmpty() ? " " : "";
+ needVisibleFull += needVisibleNormalPart;
+ }
+ String mesPrefix = needVisibleFull + ": ";
+ String mesPostfix = " in " + originalLog;
+
+ // simple check
+ Assert.assertTrue(mesPrefix + "can't find color part" + mesPostfix, originalLog.contains(needVisibleColorPart));
+ Assert.assertTrue(mesPrefix + "can't find normal part" + mesPostfix, originalLog.contains(needVisibleNormalPart));
+ Assert.assertTrue(mesPrefix + "can't find id" + mesPostfix, originalLog.contains(needId));
+
+ // html check
+ Element html = Jsoup.parse(originalLog);
+ Assert.assertEquals(mesPrefix + "can't find full text" + mesPostfix, needVisibleFull, html.text());
+ Element htmlFont = html.getElementsByTag("font").stream().findFirst().orElse(null);
+ Assert.assertNotNull(mesPrefix + "can't find tag [font]" + mesPostfix, htmlFont);
+ Assert.assertNotEquals(mesPrefix + "can't find attribute [color]" + mesPostfix, "", htmlFont.attr("color"));
+ Assert.assertEquals(mesPrefix + "can't find attribute [object_id]" + mesPostfix, needId, htmlFont.attr("object_id"));
+
+ // improved log from client (with href and popup support)
+ String popupLog = GameLog.injectPopupSupport(originalLog);
+ html = Jsoup.parse(popupLog);
+ Assert.assertEquals(mesPrefix + "injected, can't find full text" + mesPostfix, needVisibleFull, html.text());
+ // href
+ Element htmlA = html.getElementsByTag("a").stream().findFirst().orElse(null);
+ Assert.assertNotNull(mesPrefix + "injected, can't find tag [a]" + mesPostfix, htmlA);
+ Assert.assertTrue(mesPrefix + "injected, can't find attribute [href]" + mesPostfix, htmlA.attr("href").startsWith("#"));
+ Assert.assertEquals(mesPrefix + "injected, popup tag [a] must contains colored part only" + mesPostfix, needVisibleColorPart, htmlA.text());
+ // object
+ htmlFont = html.getElementsByTag("font").stream().findFirst().orElse(null);
+ Assert.assertNotNull(mesPrefix + "injected, can't find tag [font]" + mesPostfix, htmlFont);
+ Assert.assertNotEquals(mesPrefix + "can't find attribute [color]" + mesPostfix, "", htmlFont.attr("color"));
+ Assert.assertEquals(mesPrefix + "can't find attribute [object_id]" + mesPostfix, needId, htmlFont.attr("object_id"));
+ }
+
+ private void assertObjectSupport(MageObject object) {
+ String shortId = String.format("[%s]", object.getId().toString().substring(0, 3));
+
+ // original mode with both color and normal parts (name + id)
+ String log = GameLog.getColoredObjectIdName(object, null);
+ assertObjectHtmlLog(
+ log,
+ object.getName(),
+ shortId,
+ object.getId().toString()
+ );
+
+ // custom mode with both color and normal parts
+ String customName = "custom" + shortId + "name";
+ String customPart = "xxx";
+ log = GameLog.getColoredObjectIdName(object.getColor(currentGame), object.getId(), customName, customPart, null);
+ assertObjectHtmlLog(log, customName, customPart, object.getId().toString());
+
+ // custom mode with colored part only
+ customName = "custom" + shortId + "name";
+ customPart = "";
+ log = GameLog.getColoredObjectIdName(object.getColor(currentGame), object.getId(), customName, customPart, null);
+ assertObjectHtmlLog(log, customName, customPart, object.getId().toString());
+
+ // custom mode with normal part only (popup will not work in GUI due empty a-href part)
+ customName = "";
+ customPart = "xxx";
+ log = GameLog.getColoredObjectIdName(object.getColor(currentGame), object.getId(), customName, customPart, null);
+ assertObjectHtmlLog(log, customName, customPart, object.getId().toString());
+ }
+
+ @Test
+ public void test_ObjectNamesInHtml() {
+ skipInitShuffling();
+
+ addCard(Zone.HAND, playerA, "Grizzly Bears"); // card
+ addCard(Zone.BATTLEFIELD, playerA, "Mountain"); // permanent
+ addCard(Zone.COMMAND, playerA, "Soldier of Fortune"); // commander
+ // diff names
+ addCustomCardWithAbility("name normal", playerA, FlyingAbility.getInstance());
+ addCustomCardWithAbility("123", playerA, FlyingAbility.getInstance());
+ addCustomCardWithAbility("", playerA, FlyingAbility.getInstance());
+ addCustomCardWithAbility("special \" name 1", playerA, FlyingAbility.getInstance());
+ addCustomCardWithAbility("\"special name 2", playerA, FlyingAbility.getInstance());
+ addCustomCardWithAbility("special name 3\"", playerA, FlyingAbility.getInstance());
+ addCustomCardWithAbility("\"special name 4\"", playerA, FlyingAbility.getInstance());
+
+ setStrictChooseMode(true);
+ setStopAt(1, PhaseStep.DECLARE_ATTACKERS);
+ execute();
+
+ // colleat all possible objects and test logs with it
+ List sampleObjects = new ArrayList<>();
+ sampleObjects.addAll(currentGame.getBattlefield().getAllPermanents());
+ sampleObjects.addAll(playerA.getHand().getCards(currentGame));
+ sampleObjects.addAll(currentGame.getCommandersIds(playerA, CommanderCardType.ANY, false)
+ .stream()
+ .map(c -> currentGame.getObject(c))
+ .collect(Collectors.toList()));
+ Assert.assertEquals(3 + 7 + 1, sampleObjects.size()); // defaul commander game already contains +1 commander
+
+ sampleObjects.forEach(this::assertObjectSupport);
+ }
+}
diff --git a/Mage/src/main/java/mage/util/GameLog.java b/Mage/src/main/java/mage/util/GameLog.java
index b32927ba4f8..5c3d84f71e5 100644
--- a/Mage/src/main/java/mage/util/GameLog.java
+++ b/Mage/src/main/java/mage/util/GameLog.java
@@ -3,6 +3,7 @@ package mage.util;
import mage.MageObject;
import mage.ObjectColor;
+import java.util.UUID;
import java.util.regex.Pattern;
/**
@@ -54,11 +55,59 @@ public final class GameLog {
}
public static String getColoredObjectIdName(MageObject mageObject, MageObject alternativeObject) {
+ return getColoredObjectIdName(
+ mageObject.getColor(null),
+ mageObject.getId(),
+ mageObject.getName(),
+ String.format("[%s]", mageObject.getId().toString().substring(0, 3)),
+ alternativeObject == null ? null : alternativeObject.getName()
+ );
+ }
+
+ /**
+ * Prepare html text with additional object info (can be used for card popup in GUI)
+ *
+ * @param color text color of the colored part
+ * @param objectID object id
+ * @param visibleColorPart colored part, popup will be work on it
+ * @param visibleNormalPart additional part with default color
+ * @param alternativeName alternative name, popup will use it on unknown object id or name
+ * @return
+ */
+ public static String getColoredObjectIdName(ObjectColor color,
+ UUID objectID,
+ String visibleColorPart,
+ String visibleNormalPart,
+ String alternativeName) {
+ String additionalText = !visibleColorPart.isEmpty() && !visibleNormalPart.isEmpty() ? " " : "";
+ additionalText += visibleNormalPart;
return "" + mageObject.getIdName() + "";
+ + " color='" + getColorName(color) + "'"
+ + " object_id='" + objectID + "'"
+ + (alternativeName == null ? "" : " alternative_name='" + CardUtil.urlEncode(alternativeName) + "'")
+ + ">" + visibleColorPart + ""
+ + additionalText;
+ }
+
+ /**
+ * Add popup card support in game logs (client will process all href links)
+ */
+ public static String injectPopupSupport(String htmlLogs) {
+ // input/output examples:
+ // some text
+ // ignore
+ // some text
+ // ignore
+ // Mountain [233]
+ // Mountain [233]
+ // [fc7]
+ // [fc7]
+ // 16:45, T1.M1: Human puts Mountain [3d2] from hand onto the Battlefield [123]
+ // 16:45, T1.M1: Human puts Mountain [3d2] from hand onto the Battlefield [123]
+ return htmlLogs.replaceAll("
", "\r\n
\r\n").replaceAll(
+ "]*)>([^<]*)",
+ "$2"
+ );
}
public static String getColoredObjectIdNameForTooltip(MageObject mageObject) {