package magic.ui.widget.card;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import magic.data.GeneralConfig;
import magic.model.MagicCardDefinition;
import magic.model.MagicObject;
import magic.model.MagicPermanent;
import magic.ui.FontsAndBorders;
import magic.ui.MagicCardImages;
import magic.ui.MagicImages;
import magic.ui.ScreenController;
import magic.ui.duel.viewerinfo.CardViewerInfo;
import magic.ui.helpers.ImageHelper;
import magic.ui.screen.duel.game.SwingGameController;
import magic.ui.theme.AbilityIcon;
import magic.ui.widget.duel.animation.AnimationFx;
import magic.ui.widget.duel.animation.MagicAnimations;
import magic.utility.MagicSystem;
import org.pushingpixels.trident.Timeline;
import org.pushingpixels.trident.ease.Spline;
@SuppressWarnings("serial")
public class AnnotatedCardPanel extends JPanel {
private enum PopupMode {
Normal,
Prompt
}
private static final Color BCOLOR = new Color(0, 0, 0, 0);
private static final Font PT_FONT = new Font("Serif", Font.BOLD, 16);
private static final Color GRADIENT_FROM_COLOR = Color.WHITE;
private static final Color GRADIENT_TO_COLOR = Color.WHITE.darker();
private final GeneralConfig CONFIG = GeneralConfig.getInstance();
private MagicObject magicObject = null;
private Timeline fadeInTimeline;
private float opacity = 1.0f;
private SwingGameController controller;
private BufferedImage cardImage;
private String modifiedPT;
private Dimension imageOnlyPopupSize;
private Dimension popupSize;
private List<CardIcon> cardIcons = new ArrayList<>();
private final List<Shape> iconShapes = new ArrayList<>();
private Timer visibilityTimer;
private BufferedImage popupImage;
private final MagicInfoWindow infoWindow = new MagicInfoWindow();
private final Rectangle containerRect;
private boolean preferredVisibility = false;
private PopupMode popupMode = PopupMode.Normal;
public AnnotatedCardPanel() {
this.containerRect = getWindowRect();
setOpaque(false);
setDelayedVisibilityTimer();
addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent event) {
if (event.getWheelRotation() > 0) { // rotate mousewheel back or towards you.
setVisible(false);
}
}
});
setMouseMovedListener();
addMouseListener(new MouseAdapter() {
@Override
public void mouseExited(MouseEvent e) {
infoWindow.setVisible(false);
setVisible(false);
}
});
setVisible(false);
}
private static Rectangle getWindowRect() {
return new Rectangle(
ScreenController.getFrame().getLocationOnScreen(),
ScreenController.getFrame().getSize());
}
private void setDelayedVisibilityTimer() {
visibilityTimer = new Timer(0, new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
if (!AnnotatedCardPanel.this.isVisible() && preferredVisibility == true) {
showPopup();
} else if (preferredVisibility == false) {
setVisible(false);
magicObject = null;
opacity = 0f;
}
}
});
visibilityTimer.setRepeats(false);
}
public void showDelayed(final int delay) {
assert SwingUtilities.isEventDispatchThread();
preferredVisibility = true;
visibilityTimer.setInitialDelay(delay);
visibilityTimer.restart();
}
/**
* Hides the card image panel after {@code delay} milliseconds.
* <p>
* The hide request is cancelled if a request to show a card image is received
* before the delay expires (see {@link hideDelayed()}).
*
* @param delay the time in milliseconds to wait before hiding the card panel.
*/
private void hideCardPanel(int delay) {
assert SwingUtilities.isEventDispatchThread();
preferredVisibility = false;
visibilityTimer.setInitialDelay(delay);
visibilityTimer.restart();
}
/**
* Requests that the card image panel is hidden after 100 milliseconds.
* <p>
* This method is primarily used to prevent the popup flickering on and off
* as the mouse is moved over cards on the battlefield and in the player zones.
*/
public void hideDelayed() {
hideCardPanel(100);
}
public void hideNoDelay() {
hideCardPanel(0);
}
private void showPopup() {
if (MagicAnimations.isOn(AnimationFx.CARD_FADEIN)) {
if (opacity == 0f) {
fadeInTimeline = new Timeline();
fadeInTimeline.setDuration(200);
fadeInTimeline.setEase(new Spline(0.8f));
fadeInTimeline.addPropertyToInterpolate(
Timeline.property("opacity")
.on(this)
.from(0.0f)
.to(1.0f));
fadeInTimeline.play();
} else {
opacity = 1.0f;
}
} else {
opacity = 1.0f;
}
setVisible(true);
}
public void setCardForPrompt(MagicCardDefinition cardDef, Dimension containerSize) {
popupMode = PopupMode.Prompt;
this.cardImage = getCardImage(cardDef);
// <--- order important
cardIcons = AbilityIcon.getIcons(cardDef);
setPanelSize(containerSize);
// --->
this.magicObject = null;
this.modifiedPT = "";
setPopupImage();
}
public void setCard(CardViewerInfo cardInfo, final Dimension containerSize) {
popupMode = PopupMode.Normal;
this.cardImage = cardInfo.getImage();
// <--- order important
cardIcons = AbilityIcon.getIcons(cardInfo.getMagicObject());
setPanelSize(containerSize);
// --->
this.modifiedPT = getModifiedPT(cardInfo.getMagicObject());
this.magicObject = cardInfo.getMagicObject();
setPopupImage();
}
/**
* Both MagicPermanent and MagicCard are instances of MagicObject.
* MagicObject returns the correct abilities of a card including any
* additional abilities added during game-play (via enchantments, etc).
*/
public void setCard(final MagicObject magicObject, final Dimension containerSize) {
popupMode = PopupMode.Normal;
this.cardImage = getCardImage(magicObject);
// <--- order important
cardIcons = AbilityIcon.getIcons(magicObject);
setPanelSize(containerSize);
// --->
this.magicObject = magicObject;
this.modifiedPT = getModifiedPT(magicObject);
setPopupImage();
}
private void setPopupImage() {
// create a blank canvas of the appropriate size.
popupImage = ImageHelper.getCompatibleBufferedImage(getWidth(), getHeight(), Transparency.TRANSLUCENT);
final Graphics g = popupImage.getGraphics();
final Graphics2D g2d = (Graphics2D)g;
// don't overwrite original image with modified PT overlay, use a copy.
final BufferedImage cardCanvas = !modifiedPT.isEmpty() ? getImageCopy(cardImage) : cardImage;
// draw modified PT on original image so it is scaled properly.
if (magicObject != null && MagicCardImages.isProxyImage(magicObject.getCardDefinition()) == false) {
drawPowerToughnessOverlay(cardCanvas);
}
// scale card image if required.
final BufferedImage scaledImage = ImageHelper.scale(cardCanvas, imageOnlyPopupSize.width, imageOnlyPopupSize.height);
//
// draw card image onto popup canvas, right-aligned.
g.drawImage(scaledImage, popupSize.width - imageOnlyPopupSize.width, 0, this);
//
drawIcons(g2d);
if (MagicSystem.isDevMode() && magicObject != null && magicObject instanceof MagicPermanent) {
final MagicPermanent card = (MagicPermanent) magicObject;
g.setFont(FontsAndBorders.FONT1);
ImageHelper.drawStringWithOutline(g, Long.toString(card.getCard().getId()), 2, 14);
}
}
private BufferedImage getImageCopy(final BufferedImage image) {
final BufferedImage imageCopy = new BufferedImage(image.getWidth(), image.getHeight(), image.getTransparency());
final Graphics g = imageCopy.createGraphics();
g.drawImage(image, 0, 0, null);
return imageCopy;
}
private BufferedImage getPTOverlay(Color maskColor) {
// create fixed size empty transparent image.
final BufferedImage overlay = ImageHelper.getCompatibleBufferedImage(312, 445, Transparency.TRANSLUCENT);
final Graphics2D g2d = overlay.createGraphics();
// use a rectangular opaque mask to hide original P/T.
final Rectangle mask = new Rectangle(254, 408, 38, 18);
g2d.setColor(maskColor);
g2d.fillRect(mask.x, mask.y, mask.width, mask.height);
// draw the modified P/T on top of the mask.
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setColor(Color.BLACK);
g2d.setFont(PT_FONT);
final FontMetrics metrics = g2d.getFontMetrics();
final int ptWidth = metrics.stringWidth(modifiedPT);
g2d.drawString(modifiedPT, mask.x + ((mask.width - ptWidth) / 2), mask.y + 14);
g2d.dispose();
return overlay;
}
/**
* Draws modified P/T onto a 312 x 445 overlay which is then scaled to-
* and drawn on top of the original card image.
*/
private void drawPowerToughnessOverlay(final BufferedImage cardImage) {
if (modifiedPT.isEmpty())
return;
// get approximate background color of P/T box on card.
final Color maskColor = getPTOverlayBackgroundColor(
cardImage,
(int)(cardImage.getWidth() * 0.929d),
(int)(cardImage.getHeight() * 0.919d)
);
// get transparent P/T overlay and size to card image.
final BufferedImage overlay = ImageHelper.scale(
getPTOverlay(maskColor),
cardImage.getWidth(),
cardImage.getHeight()
);
// draw tranparent P/T overlay on top of original card.
final Graphics2D g2d = cardImage.createGraphics();
g2d.drawImage(overlay, 0, 0, null);
g2d.dispose();
}
private Color getPTOverlayBackgroundColor(BufferedImage image, final int x, final int y) {
final int rgb = image.getRGB(x, y);
final int r = (rgb >> 16) & 0xFF;
final int g = (rgb >> 8) & 0xFF;
final int b = (rgb & 0xFF);
return new Color(r, g, b);
}
private BufferedImage getCardImage(final MagicCardDefinition cardDef) {
return MagicImages.getCardImage(cardDef);
}
private BufferedImage getCardImage(final MagicObject magicObject) {
if (magicObject instanceof MagicPermanent) {
final MagicPermanent perm = (MagicPermanent)magicObject;
return canRevealTrueFace(perm) && perm.getCardDefinition() != perm.getRealCardDefinition() ?
getCardImage(perm.getRealCardDefinition()) :
MagicImages.getCardImage(perm);
} else {
return getCardImage(magicObject.getCardDefinition());
}
}
/**
* primarily used to determine whether a face-down card will
* show its hidden face when displaying mouse-over popup.
*/
private boolean canRevealTrueFace(final MagicPermanent perm) {
return perm.getController().isHuman() || MagicSystem.isAiVersusAi();
}
private String getModifiedPT(final MagicObject magicObject) {
if (magicObject instanceof MagicPermanent) {
final MagicPermanent permanent = (MagicPermanent)magicObject;
final String permanentPT = permanent.getPower() + "/" + permanent.getToughness();
final String cardDefPT = permanent.getCardDefinition().getCardPower() + "/" + permanent.getCardDefinition().getCardToughness();
if (!permanentPT.equals(cardDefPT)) {
return permanent.getPower() + "/" + permanent.getToughness();
} else {
return "";
}
} else {
return "";
}
}
public MagicObject getMagicObject() {
return magicObject;
}
@Override
public void paintComponent(Graphics g) {
if (opacity < 1.0f) {
final Graphics2D g2d = (Graphics2D)g;
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
g2d.setColor(BCOLOR);
Rectangle r = g2d.getClipBounds();
g2d.fillRect(r.x, r.y, r.width, r.height);
}
if (popupImage != null) {
g.drawImage(popupImage, 0, 0, this);
}
super.paintComponent(g);
}
public void setOpacity(final float opacity) {
this.opacity = opacity;
repaint();
}
public float getOpacity() {
return opacity;
}
@Override
public void setVisible(final boolean isVisible) {
super.setVisible(isVisible);
if (controller != null && CONFIG.isGamePausedOnPopup()) {
final boolean aiHasPriority = controller.getViewerInfo().getPriorityPlayer().isAi();
controller.setGamePaused(isVisible && aiHasPriority);
}
}
private void setPanelSize(final Dimension containerSize) {
final Dimension preferredSize = MagicImages.getPreferredImageSize(cardImage);
// keep scaled card in correct proportion.
final double cardAspectRatio = (double)preferredSize.height / preferredSize.width;
final int actualHeight = Math.min(containerSize.height, preferredSize.height);
final int actualWidth = (int)(actualHeight / cardAspectRatio);
final Dimension actualCardImageSize = new Dimension(actualWidth, actualHeight);
imageOnlyPopupSize = actualCardImageSize;
final Dimension annotatedPopupSize = new Dimension(actualCardImageSize.width + 26, actualCardImageSize.height);
popupSize = cardIcons.isEmpty() ? imageOnlyPopupSize : annotatedPopupSize;
//
setPreferredSize(popupSize);
setMaximumSize(popupSize);
setMinimumSize(popupSize);
setSize(popupSize);
}
private void drawIcons(final Graphics2D g2d) {
if (!cardIcons.isEmpty()) {
final int BORDER_WIDTH = 2;
final BasicStroke BORDER_STROKE = new BasicStroke(BORDER_WIDTH);
final Stroke defaultStroke = g2d.getStroke();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// draw icons
int y = 10;
int x = 0;
final int ICON_WIDTH = 36;
final int ICON_HEIGHT = 32;
final int CORNER_ARC = 16;
final GradientPaint PAINT_COLOR = new GradientPaint(0, 0, GRADIENT_FROM_COLOR, ICON_WIDTH, 0, GRADIENT_TO_COLOR);
iconShapes.clear();
for (CardIcon cardIcon : cardIcons) {
// icon bounds should be relative to CardPopupPanel.
final Rectangle2D iconShapeRect = new Rectangle2D.Double((double)x, (double)y, (double)ICON_WIDTH, 32d);
iconShapes.add(iconShapeRect);
//
final Rectangle rect = new Rectangle(x, y, ICON_WIDTH, ICON_HEIGHT);
g2d.setPaint(PAINT_COLOR);
g2d.fillRoundRect(rect.x, rect.y, rect.width, rect.height, CORNER_ARC, CORNER_ARC);
g2d.setPaint(Color.BLACK);
g2d.setStroke(BORDER_STROKE);
g2d.drawRoundRect(rect.x, rect.y, rect.width, rect.height, CORNER_ARC, CORNER_ARC);
g2d.setStroke(defaultStroke);
//
final ImageIcon icon = cardIcon.getIcon();
final int iconOffsetX = (ICON_WIDTH / 2) - (icon.getIconWidth() / 2);
final int iconOffsetY = 16 - (icon.getIconHeight() / 2);
icon.paintIcon(this, g2d, x + iconOffsetX, y + iconOffsetY);
y += ICON_HEIGHT + 1;
}
}
}
private void setMouseMovedListener() {
addMouseMotionListener(new MouseMotionAdapter() {
private CardIcon currentIcon;
@Override
public void mouseMoved(MouseEvent e) {
if (popupMode == PopupMode.Prompt) {
setVisible(false);
return;
}
if (!cardIcons.isEmpty() && !iconShapes.isEmpty()) {
final Shape lastShape = iconShapes.get(iconShapes.size() - 1);
final Dimension lastShapeSize = new Dimension(lastShape.getBounds().width, lastShape.getBounds().y + lastShape.getBounds().height);
final Rectangle rect = new Rectangle(0, 0, lastShapeSize.width, lastShapeSize.height);
if (rect.contains(e.getPoint())) {
for (Shape iconShape : iconShapes) {
if (iconShape.contains(e.getPoint())) {
final CardIcon cardIcon = cardIcons.get(iconShapes.indexOf(iconShape));
if (currentIcon != cardIcon) {
currentIcon = cardIcon;
showInfoTip(cardIcon, new Point(e.getXOnScreen(), e.getYOnScreen()));
}
break;
}
}
} else if (infoWindow.isVisible()) {
boolean hideInfo = true;
currentIcon = null;
if (hideInfo) {
infoWindow.setVisible(false);
}
}
}
}
});
}
private void showInfoTip(final CardIcon cardIcon, final Point position) {
//
infoWindow.setTitle(cardIcon.getName());
infoWindow.setDescription("<html>" + cardIcon.getDescription().replaceAll("\r\n|\r|\n", "<br>") + "</html>");
infoWindow.pack();
//
final int infoWidth = infoWindow.getWidth();
int mx = position.x + 10;
final int sx = containerRect.x;
final int sw = containerRect.width - 10;
if (mx + infoWidth > sx + sw) {
mx = mx - ((mx + infoWidth) - (sx + sw));
}
final Point p = new Point(mx, position.y + 10);
infoWindow.setLocation(p);
infoWindow.setAlwaysOnTop(true);
infoWindow.setVisible(true);
}
public void setController(SwingGameController aController) {
this.controller = aController;
this.controller.setImageCardViewer(this);
}
}