freecol/src/net/sf/freecol/client/gui/panel/InfoPanel.java

578 lines
20 KiB
Java

/**
* Copyright (C) 2002-2021 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* FreeCol is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.client.gui.panel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.LayoutManager;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
import net.sf.freecol.client.FreeColClient;
import net.sf.freecol.client.control.MapTransform;
import net.sf.freecol.client.gui.action.EndTurnAction;
import net.sf.freecol.client.gui.FontLibrary;
import net.sf.freecol.client.gui.GUI;
import net.sf.freecol.client.gui.ImageLibrary;
import net.sf.freecol.client.gui.panel.MigPanel;
import net.sf.freecol.client.gui.Size;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.model.AbstractGoods;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.GoodsContainer;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.StringTemplate;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Unit;
import static net.sf.freecol.common.util.CollectionUtils.*;
import static net.sf.freecol.common.util.StringUtils.*;
/**
* The InfoPanel is a wrapper for several informative displays in the
* lower right corner.
*/
public final class InfoPanel extends FreeColPanel
implements PropertyChangeListener {
@SuppressWarnings("unused")
private static final Logger logger = Logger.getLogger(InfoPanel.class.getName());
private static enum InfoPanelMode {
NONE, END, MAP, TILE, UNIT;
}
/** Pixel width of text area beside icon. */
private static final int TEXT_WIDTH = 150;
/** A small pixel gap. */
private static final int SLACK = 5;
/** Number of goods/production items to show. */
private static final int PRODUCTION = 4;
/** Preferred size for non-skinned panel. */
public static final Dimension PREFERRED_SIZE = new Dimension(260, 130);
/** The image library to use for the font. */
private final ImageLibrary lib;
/** The font for the end turn message. */
private final Font font;
/** An optional background image (the standard one has shape). */
private final Image skin;
/** The mouse listener for the various subpanels. */
private final MouseAdapter mouseAdapter;
/** The panel mode. */
private InfoPanelMode mode = InfoPanelMode.NONE;
/** The associated map transform when in MAP mode. */
private MapTransform mapTransform = null;
/** The associated tile when in TILE mode. */
private Tile tile = null;
/** The associated unit when in UNIT mode. */
private Unit unit = null;
/**
* The constructor that will add the items to this panel.
*
* @param freeColClient The {@code FreeColClient} for the game.
*/
public InfoPanel(final FreeColClient freeColClient) {
this(freeColClient, true);
}
/**
* The constructor that will add the items to this panel.
*
* @param freeColClient The {@code FreeColClient} for the game.
* @param useSkin Use the info panel skin.
*/
public InfoPanel(final FreeColClient freeColClient, boolean useSkin) {
super(freeColClient, null, null);
this.lib = freeColClient.getGUI().getFixedImageLibrary();
this.font = this.lib.getScaledFont("normal-plain-tiny", null);
this.skin = (useSkin) ? this.lib.getScaledImage("image.skin.InfoPanel")
: null;
if (this.skin != null) {
setBorder(null);
setSize(this.skin.getWidth(null), this.skin.getHeight(null));
// skin is output in overridden paintComponent(), which calls
// its parent, which will display panels added here
setOpaque(false);
} else {
setSize(this.lib.scale(PREFERRED_SIZE));
}
// No layout manager! Panels will be sized and placed explicitly
this.mouseAdapter = new MouseAdapter() {
/**
* {@inheritDoc}
*/
@Override
public void mousePressed(MouseEvent e) {
Tile tile = InfoPanel.this.getTile();
if (tile != null) getGUI().setFocus(tile);
}
};
}
/**
* Get a new MigPanel with specified layout and size it to fit neatly.
*
* @param layout The {@code LayoutManager} for the panel.
* @return The new {@code MigPanel}.
*/
private MigPanel newPanel(LayoutManager layout) {
MigPanel panel = new MigPanel(layout);
panel.setSize(new Dimension((int)(this.getWidth() * 0.8),
(int)(this.getHeight() * 0.6)));
return panel;
}
/**
* Size, place and request redraw of the given panel.
*
* @param panel The new panel to display.
*/
private void setPanel(MigPanel panel) {
panel.addMouseListener(this.mouseAdapter);
// Center the panel but push down a bit vertically to allow for
// the ragged top border
final int y = (this.getHeight() - panel.getHeight()/2) / 2;
panel.setLocation((this.getWidth() - panel.getWidth()) / 2, y);
if (this.skin != null) panel.setOpaque(false);
this.removeAll();
this.add(panel);
this.revalidate();
this.repaint();
}
/**
* Get the mode-dependent associated tile.
*
* @return The {@code Tile} associated with this panel.
*/
private Tile getTile() {
switch (this.mode) {
case TILE:
return this.tile;
case UNIT:
return (this.unit == null) ? null : this.unit.getTile();
default:
break;
}
return null;
}
/**
* Change the panel mode.
*
* The important job here is to clear out all the old settings.
*
* @param newMode The new {@code InfoPanelMode}.
* @return The old {@code InfoPanelMode}.
*/
private InfoPanelMode changeMode(InfoPanelMode newMode) {
InfoPanelMode oldMode = this.mode;
if (oldMode != newMode) {
switch (oldMode) {
case MAP:
this.mapTransform = null;
break;
case TILE:
this.tile = null;
break;
case UNIT:
this.unit.removePropertyChangeListener(this);
GoodsContainer gc = this.unit.getGoodsContainer();
if (gc != null) gc.removePropertyChangeListener(this);
this.unit = null;
break;
default:
break;
}
this.mode = newMode;
}
return oldMode;
}
/**
* Fill in an end turn message into a new panel and add it.
*/
private void fillEndPanel() {
MigPanel panel = newPanel(new MigLayout("wrap 1, center",
"[center]", ""));
String labelString = Messages.message("infoPanel.endTurn");
final int width = (int)(0.3 * this.getWidth());
panel.add(new JLabel("")); // hack, one blank entry at top
for (String s : splitText(labelString, " /",
getFontMetrics(this.font), width)) {
JLabel label = new JLabel(s);
label.setFont(this.font);
panel.add(label);
}
JButton button = new JButton(getFreeColClient().getActionManager()
.getFreeColAction(EndTurnAction.id));
button.setFont(this.font);
panel.add(button);
setPanel(panel);
}
/**
* Fill map transform information into a new panel and add it.
*
* @param mapTransform The {@code MapTransform} to display.
* @return The {@code MapTransform}.
*/
private MapTransform fillMapPanel(MapTransform mapTransform) {
MigPanel panel = newPanel(new BorderLayout());
final JPanel p = (mapTransform == null) ? null
: mapTransform.getDescriptionPanel();
if (p != null) {
p.setOpaque(false);
final Dimension d = p.getPreferredSize();
p.setBounds(0, (this.getHeight() - d.height)/2,
this.getWidth(), d.height);
panel.add(p, BorderLayout.CENTER);
}
setPanel(panel);
return mapTransform;
}
/**
* Fill tile information into a new panel and add it.
*
* @param tile The {@code Tile} to display.
* @return The {@code Tile}.
*/
private Tile fillTilePanel(Tile tile) {
MigPanel panel = newPanel(new MigLayout("fill, wrap " + (PRODUCTION+1) + ", gap 1 1",
"", ""));
if (tile != null) {
BufferedImage image = getGUI()
.createTileImageWithBeachBorderAndItems(tile);
if (tile.isExplored()) {
final int width = panel.getWidth() - SLACK;
String text = Messages.message(tile.getLabel());
for (String s : splitText(text, " /",
getFontMetrics(this.font), width)) {
JLabel label = new JLabel(s);
label.setFont(this.font);
panel.add(label, "span, align center");
}
panel.add(new JLabel(new ImageIcon(image)), "spany");
final Player owner = tile.getOwner();
if (owner == null) {
panel.add(new JLabel(), "span " + PRODUCTION);
} else {
StringTemplate t = owner.getNationLabel();
JLabel label = Utility.localizedLabel(t);
label.setFont(this.font);
panel.add(label, "span " + PRODUCTION);
}
JLabel defenceLabel = Utility.localizedLabel(StringTemplate
.template("infoPanel.defenseBonus")
.addAmount("%bonus%", tile.getDefenceBonusPercentage()));
defenceLabel.setFont(this.font);
panel.add(defenceLabel, "span " + PRODUCTION);
JLabel moveLabel = Utility.localizedLabel(StringTemplate
.template("infoPanel.movementCost")
.addAmount("%cost%", tile.getType().getBasicMoveCost()/3));
moveLabel.setFont(this.font);
add(moveLabel, "span " + PRODUCTION);
List<AbstractGoods> produce
= sort(tile.getType().getPossibleProduction(true),
AbstractGoods.descendingAmountComparator);
if (produce.isEmpty()) {
panel.add(new JLabel(), "span " + PRODUCTION);
} else {
for (AbstractGoods ag : produce) {
GoodsType type = ag.getType();
int n = tile.getPotentialProduction(type, null);
JLabel label = new JLabel(String.valueOf(n),
new ImageIcon(lib.getSmallGoodsTypeImage(type)),
JLabel.RIGHT);
label.setToolTipText(Messages.getName(type));
label.setFont(this.font);
panel.add(label);
}
}
} else {
panel.add(Utility.localizedLabel("unexplored"),
"span, align center");
panel.add(new JLabel(new ImageIcon(image)), "spany");
}
}
setPanel(panel);
return tile;
}
/**
* Add labels to a panel with MigLayout, on one line.
*
* @param panel The {@code JPanel} to add to.
* @param labels A list of {@code JLabel}s to add.
* @param max The maximum number of labels to put on a line
*/
private static void addLabels(JPanel panel, List<JLabel> labels, int max) {
for (;;) {
int n = Math.min(max, labels.size());
if (n <= 0) {
break;
} else if (n == 1) {
panel.add(labels.get(0));
break;
} else {
panel.add(labels.remove(0), "split " + n);
for (int i = 1; i < n; i++) panel.add(labels.remove(0));
}
}
}
/**
* Fill unit information into a new panel and add it.
*
* @param unit The {@code Unit} to display.
* @return The {@code Unit}.
*/
private Unit fillUnitPanel(Unit unit) {
ImageIcon ii = new ImageIcon(lib.getScaledUnitImage(unit));
final int width = ii.getIconWidth();
// Two columns filling whole space with no gaps
// 1. Icon of fixed width spanning full height
// 2. Text/icon fields filling the remaining horizontal space
MigPanel panel = newPanel(new MigLayout("wrap 2, fill, gap 0 0",
"[" + width + "][fill]", ""));
panel.add(new JLabel(ii), "spany, center");
String text = unit.getDescription(Unit.UnitLabelType.FULL);
JLabel textLabel;
for (String s : splitText(text, " /", getFontMetrics(this.font),
panel.getWidth() - width)) {
textLabel = new JLabel(s);
textLabel.setFont(this.font);
panel.add(textLabel);
}
text = (unit.isInEurope())
? Messages.getName(unit.getOwner().getEurope())
: Messages.message("infoPanel.moves")
+ " " + unit.getMovesAsString();
textLabel = new JLabel(text);
textLabel.setFont(this.font);
panel.add(textLabel);
if (unit.isCarrier()) {
List<JLabel> labels = new ArrayList<>();
ImageIcon icon;
JLabel label;
for (Unit carriedUnit : unit.getUnitList()) {
icon = new ImageIcon(lib.getSmallerUnitImage(carriedUnit));
label = new JLabel(icon);
text = carriedUnit.getDescription(Unit.UnitLabelType.NATIONAL);
label.setFont(this.font);
label.setToolTipText(text);
labels.add(label);
}
addLabels(panel, labels, 6); // 6 units fit well enough
labels.clear();
for (Goods goods : unit.getGoodsList()) {
int amount = goods.getAmount();
GoodsType gt = goods.getType();
icon = new ImageIcon(lib.getSmallerGoodsTypeImage(gt));
label = new JLabel(String.valueOf(amount), icon, JLabel.CENTER);
text = Messages.message(goods.getLabel(true));
label.setFont(this.font);
label.setToolTipText(text);
labels.add(label);
}
addLabels(panel, labels, 3); // goods icon+number is fits less well
}
panel.add(new JLabel(""), "growy"); // fill up remaining vertical space
setPanel(panel);
return unit;
}
/**
* Update this {@code InfoPanel} to end turn mode.
*/
public void update() {
boolean updated = false;
InfoPanelMode oldMode = changeMode(InfoPanelMode.END);
if (oldMode != InfoPanelMode.END) {
fillEndPanel();
updated = true;
}
logger.info("InfoPanel " + ((updated) ? "updated " : "maintained ")
+ oldMode + " -> " + this.mode);
}
/**
* Update this {@code InfoPanel} to map mode with a given transform.
*
* @param mapTransform The {@code MapTransform} to display.
*/
public void update(MapTransform mapTransform) {
boolean updated = false;
InfoPanelMode oldMode = changeMode(InfoPanelMode.MAP);
if (oldMode != InfoPanelMode.MAP || mapTransform != this.mapTransform) {
this.mapTransform = fillMapPanel(mapTransform);
updated = true;
}
logger.info("InfoPanel " + ((updated) ? "updated " : "maintained ")
+ oldMode + " -> " + this.mode + " with " + mapTransform);
}
/**
* Update this {@code InfoPanel} to tile mode with a given tile.
*
* @param tile The displayed {@code Tile}.
*/
public void update(Tile tile) {
boolean updated = false;
InfoPanelMode oldMode = changeMode(InfoPanelMode.TILE);
if (oldMode != InfoPanelMode.TILE || tile != this.tile) {
this.tile = fillTilePanel(tile);
updated = true;
}
logger.info("InfoPanel " + ((updated) ? "updated " : "maintained ")
+ oldMode + " -> " + this.mode + " with tile " + tile);
}
/**
* Update this {@code InfoPanel} to unit mode with a given unit.
*
* @param unit The displayed {@code Unit}.
*/
public void update(Unit unit) {
// Switch to end turn display if no active unit
if (unit == null) {
update();
return;
}
boolean updated = false;
InfoPanelMode oldMode = changeMode(InfoPanelMode.UNIT);
if (unit != this.unit) {
// Only update the PCLs when the unit changes
if (this.unit != null) {
this.unit.removePropertyChangeListener(this);
GoodsContainer gc = this.unit.getGoodsContainer();
if (gc != null) gc.removePropertyChangeListener(this);
}
unit.addPropertyChangeListener(this);
GoodsContainer gc = unit.getGoodsContainer();
if (gc != null) gc.addPropertyChangeListener(this);
}
// Always call fillUnitPanel because while the unit may not
// change, its annotations (such as moves left) might
this.unit = fillUnitPanel(unit);
updated = true;
logger.info("InfoPanel " + ((updated) ? "updated " : "maintained ")
+ oldMode + " -> " + this.mode + " with unit " + unit);
}
/**
* Refresh this panel.
*
* Apparently this is necessary when adding the info panel back into the
* canvas with the skinned corner, otherwise the unit does not get
* displayed.
* TODO: Explain why, or fix so we do not need this.
*/
public void refresh() {
switch (this.mode) {
case END:
fillEndPanel();
break;
case MAP:
fillMapPanel(this.mapTransform);
break;
case TILE:
fillTilePanel(this.tile);
break;
case UNIT:
fillUnitPanel(this.unit);
break;
default:
break;
}
}
// Override JComponent
/**
* {@inheritDoc}
*/
@Override
public void paintComponent(Graphics graphics) {
if (this.skin != null) graphics.drawImage(this.skin, 0, 0, null);
super.paintComponent(graphics);
}
// Interface PropertyChangeListener
/**
* {@inheritDoc}
*/
@Override
public void propertyChange(PropertyChangeEvent event) {
refresh();
}
}