foul-magics/Mage.Client/src/main/java/mage/client/cards/CardIconsPanel.java

408 lines
19 KiB
Java

package mage.client.cards;
import mage.abilities.icon.*;
import mage.abilities.icon.system.CombinedCountIcon;
import mage.client.components.StretchIcon;
import mage.client.dialog.PreferencesDialog;
import mage.client.util.GUISizeHelper;
import mage.util.DebugUtil;
import org.mage.card.arcane.CardRendererUtils;
import org.mage.card.arcane.ManaSymbols;
import org.mage.card.arcane.SvgUtils;
import org.mage.plugins.card.images.ImageCache;
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
* GUI panel to drawning icons (one of the card's side)
*
* @author JayDi85
*/
public class CardIconsPanel extends JPanel {
private static final CardIconPosition DEFAULT_POSITION = CardIconPosition.LEFT;
private static final CardIconOrder DEFAULT_ORDER = CardIconOrder.START;
private static final CardIconColor DEFAULT_COLOR = CardIconColor.DEFAULT; // from theme
private static final int DEFAULT_MAX_VISIBLE_COUNT = 5;
private static final int DEFAULT_ICON_SIZE_PERCENT = 30; // TODO: add support of big icons (current: wrong align, wrong size)
private static final int MINIMUM_ICON_SIZE = 32; // TODO: not working?
private static final int KEEP_ICON_IN_CARD_INSIDE_PERCENT = 70; // example: 66% - 2/3 keep inside and 1/3 keep outside
private static final int MAXIMUM_CARD_WIDTH_FOR_ICONS_SMALL_MODE = 100; // enable icons small mode for too small cards (combine ability icons to one);
private final CardIconPosition position;
private final CardIconOrder order;
private final CardIconColor color;
private final int iconSizePercent; // icons size, related to card's width
private final List<CardIcon> icons;
private final int cellsMaxCount; // split card side to cells, can be 1, 3, 5, 7 (x left + 1x center + x right)
private final int cellsVisibleCount; // if it contains too much elements then replace it by combined element (example: cells x7, visible x3)
private final int cellsOffset; // how many cells must be offset from the start and the end. Example: 0 - nothing, 1 - 1x from left and 1x from right
private int iconsGap = 3; // gap between icons in the cells (aplies from left and right sides)
private int halfSize = 0; // offset for icons from the card's border
private Font font = null;
// auto-calced for small mode, see calcSizes
private int calcedCellsMaxCount = 1;
private int calcedCellsVisibleCount = 1;
private int calcedSizeSizePercent = 30;
private int calcedCellsOffset = 1;
private CardIconPosition calcedPosition;
private CardIconOrder calcedOrder;
public CardIconsPanel(CardIconRenderSettings render) {
this(render.getCustomPosition() != null ? render.getCustomPosition() : DEFAULT_POSITION,
render.getCustomOrder() != null ? render.getCustomOrder() : DEFAULT_ORDER,
render.getCustomColor() != null ? render.getCustomColor() : DEFAULT_COLOR,
render.getCustomMaxVisibleCount() > 0 ? render.getCustomMaxVisibleCount() : DEFAULT_MAX_VISIBLE_COUNT,
render.getCustomIconSizePercent() > 0 ? render.getCustomIconSizePercent() : DEFAULT_ICON_SIZE_PERCENT
);
}
public CardIconsPanel(CardIconPosition position, CardIconOrder order, CardIconColor color, int cellsVisibleCount, int iconSizePercent) {
this(position, order, color, cellsVisibleCount, iconSizePercent, new ArrayList<>(), new Rectangle(100, 100));
}
public CardIconsPanel(CardIconPosition position, CardIconOrder order, CardIconColor color, int cellsVisibleCount, int iconSizePercent, List<CardIcon> icons, Rectangle startingCardSize) {
super(null);
this.position = (position != null ? position : DEFAULT_POSITION);
this.color = (color != null ? color : DEFAULT_COLOR);
this.iconSizePercent = iconSizePercent;
this.icons = icons;
// corners have only one icon with center order
if (this.position.getMaxIconsAmount() == 1) {
this.order = CardIconOrder.START;
this.cellsOffset = 0;
this.cellsMaxCount = 1;
} else {
this.order = order != null ? order : DEFAULT_ORDER;
this.cellsOffset = 1;
this.cellsMaxCount = 7;
}
int maxIcons = Math.max(1, Math.min(this.cellsMaxCount, cellsVisibleCount)); // must be in [1..cells];
this.cellsVisibleCount = Math.min(maxIcons, this.position.getMaxIconsAmount());
this.setVisible(false);
this.setOpaque(false);
if (DebugUtil.GUI_CARD_ICONS_DRAW_PANEL_BORDER) {
this.setBorder(BorderFactory.createLineBorder(Color.red));
}
this.updateSizes(startingCardSize);
}
public void updateSizes(Rectangle cardSize) {
this.calcSizes(cardSize);
// panel uses GridLayout with gaps, grid is static size, so the sizes structure:
// [gap + icon + gap + icon + ... gap]
// corner icons must be same sizes as max possible on left/right (for a more beautiful look)
int panelFullSize = this.halfSize * 2 + cardSize.height;
int panelIconSize = (panelFullSize - (7 + 1) * this.iconsGap) / 7;
int cornerHalfSize = Math.min(panelIconSize, this.halfSize * 2) / 2; // real icons can be limited by height or width
// move panel to the inner (for a more beautiful look)
// 2/3 keep inside and 1/3 keep outside
// panels already centered by halfSize, so use "- this.halfSize"
int panelOffset = Math.round(this.halfSize * 2 * KEEP_ICON_IN_CARD_INSIDE_PERCENT / 100f) - this.halfSize;
Rectangle panelRect;
Point panelTranslate;
switch (this.calcedPosition) {
case TOP:
panelRect = new Rectangle(cardSize.x - this.halfSize, cardSize.y - this.halfSize, cardSize.width + this.halfSize * 2, this.halfSize * 2);
panelTranslate = new Point(0, panelOffset);
this.setLayout(new GridLayout(1, this.calcedCellsMaxCount, iconsGap, 0));
break;
case LEFT:
panelRect = new Rectangle(cardSize.x - this.halfSize, cardSize.y - this.halfSize, this.halfSize * 2, cardSize.height + this.halfSize * 2);
panelTranslate = new Point(panelOffset, 0);
this.setLayout(new GridLayout(this.calcedCellsMaxCount, 1, 0, iconsGap));
break;
case RIGHT:
panelRect = new Rectangle(cardSize.x + cardSize.width - this.halfSize, cardSize.y - this.halfSize, this.halfSize * 2, cardSize.height + this.halfSize * 2);
panelTranslate = new Point(-panelOffset, 0);
this.setLayout(new GridLayout(this.calcedCellsMaxCount, 1, 0, iconsGap));
break;
case BOTTOM:
panelRect = new Rectangle(cardSize.x - this.halfSize, cardSize.y + cardSize.height - this.halfSize, cardSize.width + this.halfSize * 2, this.halfSize * 2);
panelTranslate = new Point(0, -panelOffset);
this.setLayout(new GridLayout(1, this.calcedCellsMaxCount, iconsGap, 0));
break;
case CORNER_TOP_LEFT:
panelRect = new Rectangle(cardSize.x - cornerHalfSize, cardSize.y - cornerHalfSize, cornerHalfSize * 2, cornerHalfSize * 2);
panelTranslate = new Point(panelOffset, panelOffset);
this.setLayout(new GridLayout(1, 1, 0, 0));
break;
case CORNER_TOP_RIGHT:
panelRect = new Rectangle(cardSize.x + cardSize.width - cornerHalfSize, cardSize.y - cornerHalfSize, cornerHalfSize * 2, cornerHalfSize * 2);
panelTranslate = new Point(-panelOffset, panelOffset);
this.setLayout(new GridLayout(1, 1, 0, 0));
break;
case CORNER_BOTTOM_LEFT:
panelRect = new Rectangle(cardSize.x - cornerHalfSize, cardSize.y + cardSize.height - cornerHalfSize, cornerHalfSize * 2, cornerHalfSize * 2);
panelTranslate = new Point(panelOffset, -panelOffset);
this.setLayout(new GridLayout(1, 1, 0, 0));
break;
case CORNER_BOTTOM_RIGHT:
panelRect = new Rectangle(cardSize.x + cardSize.width - cornerHalfSize, cardSize.y + cardSize.height - cornerHalfSize, cornerHalfSize * 2, cornerHalfSize * 2);
panelTranslate = new Point(-panelOffset, -panelOffset);
this.setLayout(new GridLayout(1, 1, 0, 0));
break;
default:
throw new IllegalArgumentException("Card icons do not support position " + this.calcedPosition);
}
panelRect.translate(panelTranslate.x, panelTranslate.y);
this.setBounds(panelRect);
// reload icons for new size
this.updateIcons();
}
public void updateIcons() {
updateIcons(null);
}
public void updateIcons(List<CardIcon> newIcons) {
this.removeAll();
if (newIcons != null) {
this.icons.clear();
TreeMap<CardIconType, List<CardIcon>> cardIconMap = new TreeMap<>();
newIcons.forEach(icon -> cardIconMap.computeIfAbsent(icon.getIconType(), k -> new ArrayList<>()).add(icon));
for (Map.Entry<CardIconType, List<CardIcon>> entry : cardIconMap.entrySet()) {
List<CardIcon> combined = entry.getValue()
.stream()
.filter(icon -> icon != null && icon.canBeCombined())
.sorted(CardIconComparator.instance)
.collect(Collectors.toList());
if (combined.size() > 1) {
entry.getValue().removeAll(combined);
String combinedHint = combined.stream()
.map(CardIcon::getCombinedInfo)
.collect(Collectors.joining("<br>"));
CardIcon combinedIcon = new CardIconImpl(entry.getKey(), combinedHint);
this.icons.add(combinedIcon);
this.icons.addAll(entry.getValue());
} else {
this.icons.addAll(entry.getValue());
}
}
}
// auto-hide panel on empty icons
if (this.icons.isEmpty()) {
this.setVisible(false);
return;
} else {
this.setVisible(true);
}
int actualMaxVisibleCount = Math.min(this.calcedCellsVisibleCount, this.calcedCellsMaxCount - this.calcedCellsOffset * 2); // preserve offset cells
List<Component> visibleComponents = new ArrayList<>();
List<Component> combinedComponents = new ArrayList<>();
List<Component> orderedComponents = new ArrayList<>();
// structure:
// * icons panel - control the icons size and offsets in card;
// * grid panel - control the icons order and position (put empty panel for empty space)
// * grid's cell - control one icon
// * label - stretched icon image that occupy all cell's space
Map<Component, CardIcon> cardLinks = new HashMap<>();
this.icons.stream()
.sorted(CardIconComparator.instance)
.forEach(icon -> {
Component iconComponent = createIconComponent(icon);
if (iconComponent != null) {
visibleComponents.add(iconComponent);
cardLinks.put(iconComponent, icon);
}
});
// OPTIMIZE visible components (if card contains too much icons then combine it in one "...")
if (visibleComponents.size() > actualMaxVisibleCount) {
while (visibleComponents.size() > actualMaxVisibleCount - 1) {
// combined must contains minimum 2 elements
combinedComponents.add(visibleComponents.remove(visibleComponents.size() - 1));
}
String combinedHint = combinedComponents
.stream()
.map(cardLinks::get)
.filter(Objects::nonNull)
.sorted(CardIconComparator.instance)
.map(CardIcon::getCombinedInfo)
.collect(Collectors.joining("<br>"));
CardIcon combinedIcon = new CombinedCountIcon(combinedComponents.size(), combinedHint);
Component combinedComponent = createIconComponent(combinedIcon);
if (combinedComponent != null) {
visibleComponents.add(combinedComponent);
}
}
// add offsets to the start of the list
if (this.calcedOrder == CardIconOrder.START || this.calcedOrder == CardIconOrder.END) {
for (int i = 0; i < this.calcedCellsOffset; i++) {
JPanel panel = new JPanel(null);
panel.setOpaque(false);
visibleComponents.add(0, panel);
}
}
// fill components list to max (grid can't put elements to cells, so must fill all)
while (visibleComponents.size() < this.calcedCellsMaxCount) {
JPanel panel = new JPanel(null);
panel.setOpaque(false);
visibleComponents.add(panel);
}
// ORDER visible components
// icons sort order example with CENTER order:
// 1: [1]
// 3: [2 1 3]
// 5: [4 2 1 3 5]
// 7: [6 4 2 1 3 5 7]
//
// icons sort order example with START order (END order is same but reversed):
// 1: [1]
// 3: [1 2 3]
// 5: [1 2 3 4 5]
// 7: [1 2 3 4 5 6 7]
if (this.calcedOrder == CardIconOrder.CENTER) {
// CENTER
if (this.calcedCellsMaxCount == 1) {
Arrays.asList(1).forEach(i -> orderedComponents.add(visibleComponents.get(i - 1)));
} else if (this.calcedCellsMaxCount == 3) {
Arrays.asList(2, 1, 3).forEach(i -> orderedComponents.add(visibleComponents.get(i - 1)));
} else if (this.calcedCellsMaxCount == 5) {
Arrays.asList(4, 2, 1, 3, 5).forEach(i -> orderedComponents.add(visibleComponents.get(i - 1)));
} else if (this.calcedCellsMaxCount == 7) {
Arrays.asList(6, 4, 2, 1, 3, 5, 7).forEach(i -> orderedComponents.add(visibleComponents.get(i - 1)));
} else {
throw new IllegalArgumentException("Card icons do not support max size as " + this.calcedCellsMaxCount);
}
} else if (this.calcedOrder == CardIconOrder.START) {
// START
orderedComponents.addAll(visibleComponents);
} else if (this.calcedOrder == CardIconOrder.END) {
// END
orderedComponents.addAll(visibleComponents);
Collections.reverse(orderedComponents);
} else {
throw new IllegalArgumentException("Card icons do not support order type " + this.calcedOrder);
}
// ADD real components to the grid
orderedComponents.forEach(this::add);
}
private Component createIconComponent(CardIcon icon) {
if (!SvgUtils.haveSvgSupport()) {
return null;
}
// direct call
//BufferedImage iconImage = ImageManagerImpl.instance.getCardIcon(icon.getIconType().getResourceName(), this.halfSize * 2);
// cached call
BufferedImage iconImageCached = ImageCache.getCardIconImage(
icon.getIconType().getResourceName(),
this.halfSize * 2,
color.toString()
).getImage();
if (iconImageCached != null && this.font != null) {
BufferedImage iconImageWithText = ImageManagerImpl.deepCopy(iconImageCached); // must copy cached value before modify
// text
String hint = ManaSymbols.replaceSymbolsWithHTML(icon.getHint(), ManaSymbols.Type.CARD_ICON_HINT);
hint = GUISizeHelper.textToHtmlWithSize(hint, GUISizeHelper.cardTooltipFont.getSize());
JLabel label = new JLabel();
label.setToolTipText("<html>" + hint);
if (!icon.getText().isEmpty()) {
Graphics2D g2 = iconImageWithText.createGraphics();
try {
g2.setColor(PreferencesDialog.getCurrentTheme().getCardIconsTextColor(this.color));
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Rectangle rect = CardRendererUtils.reduceRect(new Rectangle(0, 0, iconImageWithText.getWidth(), iconImageWithText.getHeight()), 0.8f);
CardRendererUtils.drawCenteredText(g2, icon.getText(), rect, this.font, true);
} finally {
g2.dispose();
}
}
// the stretch icon can occupy all space (full grid's cell)
StretchIcon s = new StretchIcon(iconImageWithText, true);
label.setIcon(s);
label.setIconTextGap(0);
if (DebugUtil.GUI_CARD_ICONS_DRAW_ICON_BORDER) {
label.setBorder(BorderFactory.createLineBorder(Color.green));
}
return label;
}
return null;
}
private void calcSizes(Rectangle cardSize) {
// small mode takes 20% of card's sizes diff
boolean smallMode = false;
int maxW = GUISizeHelper.battlefieldCardMaxDimension.width;
int minW = GUISizeHelper.battlefieldCardMinDimension.width;
if (minW > maxW) {
// on wrong settings
maxW = GUISizeHelper.battlefieldCardMinDimension.width;
minW = GUISizeHelper.battlefieldCardMaxDimension.width;
}
// cardSize.width < 120 - disable small mode on too big cards
if (cardSize.width < MAXIMUM_CARD_WIDTH_FOR_ICONS_SMALL_MODE && (cardSize.width < minW + (maxW - minW) * 0.2f)) {
smallMode = true;
}
// auto-sizeable icons (smaller for small card, normal for big)
this.calcedSizeSizePercent = this.iconSizePercent;
if (smallMode) {
this.calcedSizeSizePercent = Math.round(this.calcedSizeSizePercent * 1.5f);
}
// auto-amount for icons (less for small, normal for big)
this.calcedCellsMaxCount = this.cellsMaxCount;
this.calcedCellsVisibleCount = this.cellsVisibleCount;
this.calcedCellsOffset = this.cellsOffset;
this.calcedPosition = this.position;
this.calcedOrder = this.order;
if (smallMode) {
this.calcedCellsMaxCount = Math.min(5, this.calcedCellsMaxCount);
this.calcedCellsVisibleCount = Math.min(1, this.calcedCellsVisibleCount);
this.calcedCellsOffset = Math.min(1, this.calcedCellsOffset);
// change order of multi-icons and ignore corners (make icons it centered)
if (this.calcedPosition.getMaxIconsAmount() > 1) {
this.calcedOrder = CardIconOrder.CENTER;
}
}
// REAL SIZES
// auto-sizeable gaps (use test render form to find best values)
this.iconsGap = Math.floorDiv(cardSize.width, 100) * 2;
// icons intersect the card like mtg arena
this.halfSize = Math.max(MINIMUM_ICON_SIZE / 2, Math.round(cardSize.width / 100.0f * this.calcedSizeSizePercent / 2.0f));
this.font = new Font("Arial", Font.PLAIN + Font.BOLD, Math.round(this.halfSize * 1.5f));
}
public int getHalfSize() {
return halfSize;
}
}