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) {