GUI - new card hints window features:

* new help window can be opened from a player panel;
* it collect and show all visible game hints from all players and all zones;
* it updates in real time on game update;
* allows to customize visible data;
* allows to open multiple windows (current limit is 5 windows, can be slow to render);
* allows to minimize opened windows;
* workable card popup on mouse move over card name or card id;
* filter modes:
  * all - show hints from all players;
  * player - show hints from single player;
* group mode:
  * by hints - show same hints as one with all used cards;
  * by cards - show full cards list with own hints;
* search mode:
  * allows to filter card hints by player name, card name, card id or card hint;
  * allows to search multiple words (equals to "or")
* current limitation:
  * card popup shows a card instead a real object, e.g. miss card hints in it (relelated to game logs problem);
  * unsupport of emblems, dungeons and other non card objects from a command zone;
  * unsupport of revealed and library's top cards;

GUI - player's panel improves:
* added hints helper button;
* added player hithlight as possible target in choose dialogs;
* improved player name button in small mode;
* fixed wrong height in small mode;

Other fixes:
* game logs: added card popup support for logs with custom object name;
This commit is contained in:
Oleg Agafonov 2023-11-18 14:48:25 +04:00
parent ca80849249
commit 2bbe2b3c43
16 changed files with 1229 additions and 75 deletions

View file

@ -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

View file

@ -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

View file

@ -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("(<font color=[^>]*>([^<]*)) (\\[[0-9a-fA-F]*\\])</font>", "<a href=\"#$2\">$1</a> $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();
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.3" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JInternalFrameFormInfo">
<Properties>
<Property name="closable" type="boolean" value="true"/>
<Property name="iconifiable" type="boolean" value="true"/>
<Property name="resizable" type="boolean" value="true"/>
<Property name="title" type="java.lang.String" value="Card hints helper"/>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[200, 100]"/>
</Property>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[400, 300]"/>
</Property>
<Property name="titelBarToolTip" type="java.lang.String" value="&lt;Not Set&gt;"/>
</Properties>
<SyntheticProperties>
<SyntheticProperty name="formSizePolicy" type="int" value="1"/>
</SyntheticProperties>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,79,0,0,1,52"/>
</AuxValues>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
<SubComponents>
<Container class="javax.swing.JScrollPane" name="scrollView">
<Properties>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="null"/>
</Property>
<Property name="opaque" type="boolean" value="false"/>
</Properties>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
<BorderConstraints direction="Center"/>
</Constraint>
</Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Component class="mage.client.components.ColorPane" name="hintsView">
<Properties>
<Property name="editable" type="boolean" value="false"/>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo">
<EmptyBorder bottom="5" left="5" right="5" top="5"/>
</Border>
</Property>
<Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor">
<Font name="Arial" size="14" style="0"/>
</Property>
<Property name="text" type="java.lang.String" value="&lt;html&gt; test text"/>
<Property name="focusable" type="boolean" value="false"/>
<Property name="opaque" type="boolean" value="false"/>
</Properties>
</Component>
</SubComponents>
</Container>
<Container class="javax.swing.JPanel" name="commands">
<Properties>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo">
<EmptyBorder bottom="5" left="5" right="5" top="5"/>
</Border>
</Property>
</Properties>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
<BorderConstraints direction="North"/>
</Constraint>
</Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridLayout">
<Property name="columns" type="int" value="3"/>
<Property name="horizontalGap" type="int" value="5"/>
<Property name="rows" type="int" value="1"/>
<Property name="verticalGap" type="int" value="5"/>
</Layout>
<SubComponents>
<Component class="javax.swing.JComboBox" name="comboFilterBy">
<Properties>
<Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
<StringArray count="4">
<StringItem index="0" value="Item 1"/>
<StringItem index="1" value="Item 2"/>
<StringItem index="2" value="Item 3"/>
<StringItem index="3" value="Item 4"/>
</StringArray>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;String&gt;"/>
</AuxValues>
</Component>
<Component class="javax.swing.JComboBox" name="comboGroupBy">
<Properties>
<Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
<StringArray count="4">
<StringItem index="0" value="Item 1"/>
<StringItem index="1" value="Item 2"/>
<StringItem index="2" value="Item 3"/>
<StringItem index="3" value="Item 4"/>
</StringArray>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;String&gt;"/>
</AuxValues>
</Component>
<Container class="javax.swing.JPanel" name="searchPanel">
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
<SubComponents>
<Component class="javax.swing.JTextField" name="search">
<Properties>
<Property name="text" type="java.lang.String" value="search"/>
</Properties>
<Events>
<EventHandler event="focusGained" listener="java.awt.event.FocusListener" parameters="java.awt.event.FocusEvent" handler="searchFocusGained"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
<BorderConstraints direction="Center"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JButton" name="searchClear">
<Properties>
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
<Image iconType="3" name="/buttons/sideboard_out.png"/>
</Property>
<Property name="toolTipText" type="java.lang.String" value="Clear search field"/>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[23, 23]"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="searchClearActionPerformed"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
<BorderConstraints direction="East"/>
</Constraint>
</Constraints>
</Component>
</SubComponents>
</Container>
</SubComponents>
</Container>
</SubComponents>
</Form>

View file

@ -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 = "<br>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<CardHintInfo> 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<String> 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<String> 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<String> 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<String> 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<String> 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<CardHintInfo> 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<String> needSearches = new ArrayList<>(Arrays.asList(this.currentSearch.toLowerCase(Locale.ENGLISH).trim().split(" ")));
filteredCards.removeIf(info -> !(info.containsText(needSearches)));
}
// apply group type
List<String> 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<String, List<CardHintInfo>> groupsByZone = prepareGroupByPlayerAndZones(filteredCards, true, false);
groupsByZone.forEach((group, groupCards) -> {
if (groupCards.isEmpty()) {
return;
}
if (!renderData.isEmpty()) {
renderData.add("<br>");
}
// header: player
CardHintInfo sampleData = groupCards.get(0);
renderData.add(String.format("<b>%s</b>:",
GameLog.getColoredPlayerName(sampleData.player == null ? "watcher" : sampleData.player.getName())
));
// data: unique hints
Map<String, List<CardHintInfo>> 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<String, List<CardHintInfo>> groupsByZone = prepareGroupByPlayerAndZones(filteredCards, true, true);
groupsByZone.forEach((group, groupCards) -> {
if (groupCards.isEmpty()) {
return;
}
if (!renderData.isEmpty()) {
renderData.add("<br>");
}
// header: player/zone
CardHintInfo sampleData = groupCards.get(0);
renderData.add(String.format("<b>%s - %s</b>:",
GameLog.getColoredPlayerName(sampleData.player == null ? "watcher" : sampleData.player.getName()),
sampleData.zone
));
// data: cards list
groupCards.forEach(info -> {
List<String> 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("<br>", renderData);
// keep scrolls position between updates
int oldPos = this.scrollView.getVerticalScrollBar().getValue();
this.hintsView.setText("<html>" + ManaSymbols.replaceSymbolsWithHTML(renderFinal, ManaSymbols.Type.CHAT));
SwingUtilities.invokeLater(() -> {
this.scrollView.getVerticalScrollBar().setValue(oldPos);
});
updateTitle();
showAndPositionWindow();
}
private Map<String, List<CardHintInfo>> prepareGroupByPlayerAndZones(
List<CardHintInfo> currentHints,
boolean groupByPlayer,
boolean groupByZone
) {
if (!groupByPlayer && !groupByZone) {
throw new IllegalArgumentException("Wrong code usage: must use minimum one group param");
}
Map<String, List<CardHintInfo>> res = new LinkedHashMap<>();
if (currentHints.isEmpty()) {
return res;
}
String lastGroupName = null;
List<CardHintInfo> 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<String, List<CardHintInfo>> prepareGroupByHints(List<CardHintInfo> currentHints) {
Map<String, List<CardHintInfo>> res = new LinkedHashMap<>();
if (currentHints.isEmpty()) {
return res;
}
// unique and sorted hints
List<String> allPossibleHints = currentHints
.stream()
.map(info -> parseCardHints(info.card.getRules()))
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.distinct()
.sorted()
.collect(Collectors.toList());
allPossibleHints.forEach(currentHintName -> {
List<CardHintInfo> 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<String> parseCardHints(List<String> rules) {
if (rules.isEmpty() || !rules.contains(HintUtils.HINT_START_MARK)) {
return null;
}
List<String> 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")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//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("<Not Set>");
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("<html> 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();
}// </editor-fold>//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<String> comboFilterBy;
private javax.swing.JComboBox<String> 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
}

View file

@ -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;

View file

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

View file

@ -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<String, Serializable> options,
JPopupMenu popupMenu, Listener<Event> 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() {

View file

@ -86,6 +86,8 @@ public final class GamePanel extends javax.swing.JPanel {
private final Map<String, CardInfoWindowDialog> sideboardWindows = new HashMap<>();
private final ArrayList<ShowCardsDialog> pickTarget = new ArrayList<>();
private final ArrayList<PickPileDialog> pickPile = new ArrayList<>();
private final Map<String, CardHintsHelperDialog> 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;

View file

@ -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));
}
}

View file

@ -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 {
}// </editor-fold>//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;

View file

@ -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) + ']';
}
}

View file

@ -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<MageObject> 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);
}
}

View file

@ -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 "<font"
+ " color='" + getColorName(mageObject.getColor(null)) + "'"
+ " object_id='" + mageObject.getId() + "'"
+ (alternativeObject == null ? "" : " alternative_name='" + CardUtil.urlEncode(alternativeObject.getName()) + "'")
+ ">" + mageObject.getIdName() + "</font>";
+ " color='" + getColorName(color) + "'"
+ " object_id='" + objectID + "'"
+ (alternativeName == null ? "" : " alternative_name='" + CardUtil.urlEncode(alternativeName) + "'")
+ ">" + visibleColorPart + "</font>"
+ 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
// <font color='red'>some text</font>
// ignore
// <font color='#B0C4DE' object_id='xxx'>Mountain</font> [233]
// <a href="#Mountain"><font color='#B0C4DE' object_id='xxx'>Mountain</font></a> [233]
// <font color='#FF6347' object_id='xxx'>[fc7]</font>
// <a href="#[fc7]"><font color='#FF6347' object_id='xxx'>[fc7]</font></a>
// <font color='#CCCC33'>16:45, T1.M1: </font><font color='White'><font color='#20B2AA'>Human</font> puts <font color='#B0C4DE' object_id='3d2cae7c-9785-47b6-a636-84b07d939425'>Mountain</font> [3d2] from hand onto the Battlefield</font> [123]
// <font color='#CCCC33'>16:45, T1.M1: </font><font color='White'><font color='#20B2AA'>Human</font> puts <a href="#Mountain"><font color='#B0C4DE' object_id='3d2cae7c-9785-47b6-a636-84b07d939425'>Mountain</font></a> [3d2] from hand onto the Battlefield</font> [123]
return htmlLogs.replaceAll("<br>", "\r\n<br>\r\n").replaceAll(
"<font (color=[^<]*object_id=[^>]*)>([^<]*)</font>",
"<a href=\"#$2\"><font $1>$2</font></a>"
);
}
public static String getColoredObjectIdNameForTooltip(MageObject mageObject) {