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

785 lines
26 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.Component;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.function.Function;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.TransferHandler;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.PanelUI;
import net.miginfocom.swing.MigLayout;
import net.sf.freecol.client.FreeColClient;
import net.sf.freecol.client.gui.ImageLibrary;
import net.sf.freecol.client.gui.label.FreeColLabel;
import net.sf.freecol.client.gui.label.GoodsTypeLabel;
import net.sf.freecol.client.gui.plaf.FreeColSelectedPanelUI;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.StringTemplate;
import net.sf.freecol.common.model.TradeLocation;
import net.sf.freecol.common.model.TradeRoute;
import net.sf.freecol.common.model.TradeRouteStop;
import static net.sf.freecol.common.util.CollectionUtils.*;
/**
* Allows the user to edit trade routes.
*/
public final class TradeRouteInputPanel extends FreeColPanel
implements ListSelectionListener {
private static final Logger logger = Logger.getLogger(TradeRouteInputPanel.class.getName());
public static final DataFlavor STOP_FLAVOR
= new DataFlavor(TradeRouteStop.class, "Stop");
private class DestinationCellRenderer extends JLabel
implements ListCellRenderer<String> {
public DestinationCellRenderer() {
setOpaque(true);
}
/**
* {@inheritDoc}
*/
@Override
public Component getListCellRendererComponent(JList<? extends String> list,
String value,
int index,
boolean isSelected,
boolean cellHasFocus) {
FreeColGameObject fcgo = getGame().getFreeColGameObject(value);
if (fcgo instanceof Location) {
setText(Messages.message(((Location)fcgo).getLocationLabel()));
} else {
setText(value);
}
setForeground((isSelected) ? list.getSelectionForeground()
: list.getForeground());
setBackground((isSelected) ? list.getSelectionBackground()
: list.getBackground());
return this;
}
}
/**
* Panel for all types of goods that can be loaded onto a carrier.
* This should *not* change under drag/drop, but a drop indicates
* that the dropped goods type is no longer needed to be loaded
* at the currently selected stop.
*/
private class AllGoodsTypesPanel extends GoodsTypePanel {
/** The currently selected goods types. */
private List<GoodsTypeLabel> labels;
public AllGoodsTypesPanel(List<GoodsTypeLabel> labels) {
super(new GridLayout(0, 4, MARGIN, MARGIN), true);
this.labels = labels;
setOpaque(false);
setBorder(Utility.localizedBorder("goods"));
reset();
}
public void reset() {
setLabels(this.labels);
}
/**
* {@inheritDoc}
*/
@Override
public void setEnabled(boolean enable) {
super.setEnabled(enable);
for (Component child : getComponents()) {
if (child instanceof GoodsTypeLabel) child.setEnabled(enable);
}
}
// Interface DropTarget
/**
* {@inheritDoc}
*/
@Override
public Component add(Component comp, boolean editState) {
Component ret = super.add(comp);
if (comp instanceof GoodsTypeLabel) {
GoodsTypeLabel gtl = (GoodsTypeLabel)comp;
cancelImport(gtl.getType());
}
return ret;
}
}
/**
* Panel for the types of goods that are to be loaded onto the carrier
* at the current stop. This changes under drag/drop, and a drop
* adds the dropped goods type to be loaded at the currently selected
* stop.
*/
private class StopGoodsTypesPanel extends GoodsTypePanel {
public StopGoodsTypesPanel() {
super(new MigLayout("wrap 10"), false);
}
// Interface DropTarget
/**
* {@inheritDoc}
*/
@Override
public Component add(Component comp, boolean editState) {
Component ret = super.add(comp);
if (ret instanceof GoodsTypeLabel) {
GoodsTypeLabel gtl = (GoodsTypeLabel)ret;
enableImport(gtl.getType());
}
return ret;
}
}
private static class StopListTransferable implements Transferable {
private final List<TradeRouteStop> stops;
public StopListTransferable(List<TradeRouteStop> stops) {
this.stops = stops;
}
public List<TradeRouteStop> getStops() {
return stops;
}
// Interface Transferable
/**
* {@inheritDoc}
*/
@Override
public Object getTransferData(DataFlavor flavor) {
return (flavor == STOP_FLAVOR) ? stops : null;
}
/**
* {@inheritDoc}
*/
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { STOP_FLAVOR };
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavor == STOP_FLAVOR;
}
}
/**
* TransferHandler for Stops.
*/
private class StopListHandler extends TransferHandler {
/**
* {@inheritDoc}
*/
@Override
public boolean canImport(JComponent c, DataFlavor[] flavors) {
return any(flavors, matchKeyEquals(STOP_FLAVOR));
}
/**
* {@inheritDoc}
*/
@Override
protected Transferable createTransferable(JComponent c) {
JList list = (JList)c;
final DefaultListModel model = (DefaultListModel)list.getModel();
int[] indicies = list.getSelectedIndices();
List<TradeRouteStop> stops = new ArrayList<>(indicies.length);
for (int i : indicies) stops.add(i, (TradeRouteStop)model.get(i));
return new StopListTransferable(stops);
}
/**
* {@inheritDoc}
*/
@Override
public int getSourceActions(JComponent c) {
return MOVE;
}
/**
* {@inheritDoc}
*/
@Override
public boolean importData(JComponent target, Transferable data) {
JList<TradeRouteStop> stl = TradeRouteInputPanel.this.stopList;
if (target == stl
&& data instanceof StopListTransferable
&& canImport(target, data.getTransferDataFlavors())) {
List<TradeRouteStop> stops
= ((StopListTransferable)data).getStops();
DefaultListModel<TradeRouteStop> model
= new DefaultListModel<>();
int index = stl.getMaxSelectionIndex();
for (TradeRouteStop stop : stops) {
if (index < 0) {
model.addElement(stop);
} else {
index++;
model.add(index, stop);
}
}
stl.setModel(model);
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected void exportDone(JComponent source, Transferable data,
int action) {
try {
if (source instanceof JList && action == MOVE) {
JList stopList = (JList)source;
DefaultListModel listModel
= (DefaultListModel)stopList.getModel();
for (Object o : (List) data.getTransferData(STOP_FLAVOR)) {
listModel.removeElement(o);
}
}
} catch (Exception e) {
logger.warning(e.toString());
}
}
}
private class StopRenderer implements ListCellRenderer<TradeRouteStop> {
private final JPanel SELECTED_COMPONENT = new JPanel();
private final JPanel NORMAL_COMPONENT = new JPanel();
public StopRenderer() {
NORMAL_COMPONENT.setLayout(new MigLayout("", "[80, center][]"));
NORMAL_COMPONENT.setOpaque(false);
SELECTED_COMPONENT.setLayout(new MigLayout("", "[80, center][]"));
SELECTED_COMPONENT.setOpaque(false);
SELECTED_COMPONENT.setUI((PanelUI)FreeColSelectedPanelUI
.createUI(SELECTED_COMPONENT));
}
/**
* {@inheritDoc}
*/
@Override
public Component getListCellRendererComponent(JList<? extends TradeRouteStop> list,
TradeRouteStop value,
int index,
boolean isSelected,
boolean hasFocus) {
JPanel panel = (isSelected) ? SELECTED_COMPONENT
: NORMAL_COMPONENT;
panel.removeAll();
panel.setForeground(list.getForeground());
panel.setFont(list.getFont());
Location location = value.getLocation();
ImageLibrary lib = getImageLibrary();
ImageIcon ii;
JLabel name;
if (location instanceof TradeLocation) {
TradeLocation tl = (TradeLocation) location;
if (tl.canBeInput() == true) {
name = tl.getNameAsJlabel();
} else {
throw new IllegalStateException("Bogus location: " + location);
}
} else {
throw new IllegalStateException("Bogus location: " + location);
}
if (location instanceof Europe) {
Player owner = ((Europe)location).getOwner();
ii = new ImageIcon(lib.getSmallerNationImage(owner.getNation()));
} else if (location instanceof Colony) {
Colony colony = (Colony) location;
ii = new ImageIcon(lib.getSmallerSettlementImage(colony));
} else {
throw new IllegalStateException("Bogus location: " + location);
}
panel.add(new JLabel(ii), "spany");
panel.add(name, "span, wrap");
for (GoodsType cargo : value.getCargo()) {
ii = new ImageIcon(lib.getSmallerGoodsTypeImage(cargo));
panel.add(new JLabel(ii));
}
return panel;
}
}
/**
* The original route passed to this panel. We are careful not to
* modify it until we are sure all is well.
*/
private final TradeRoute newRoute;
/** The TransferHandler for the cargo labels. */
private TransferHandler transferHandler;
/** Mouse listener to use throughout. */
private transient MouseListener dragListener;
/** Model to contain the current stops. */
private DefaultListModel<TradeRouteStop> stopListModel;
/** The list of stops to show. */
private JList<TradeRouteStop> stopList;
/** The user-editable name of the trade route. */
private JTextField tradeRouteName;
/** A box to select stops to add. */
private JComboBox<String> destinationSelector;
/** Toggle message display. */
private JCheckBox messagesBox;
/** A button to add stops with. */
private JButton addStopButton;
/** A button to remove stops with. */
private JButton removeStopButton;
/** The panel displaying all goods types that could be transported. */
private AllGoodsTypesPanel allGoodsTypesPanel;
/** The panel displaying the goods types to collect at the selected stop. */
private StopGoodsTypesPanel stopGoodsTypesPanel;
/**
* Create a panel to define trade route cargos.
*
* @param freeColClient The {@code FreeColClient} for the game.
* @param newRoute The {@code TradeRoute} to operate on.
*/
public TradeRouteInputPanel(FreeColClient freeColClient,
TradeRoute newRoute) {
super(freeColClient, null,
new MigLayout("wrap 4, fill", "[]20[fill]rel"));
final Game game = freeColClient.getGame();
final Player player = getMyPlayer();
final TradeRoute tradeRoute = newRoute.copy(game);
this.newRoute = newRoute;
this.transferHandler = new DefaultTransferHandler(freeColClient, this);
this.dragListener = new DragListener(freeColClient, this);
this.stopListModel = new DefaultListModel<>();
for (TradeRouteStop stop : tradeRoute.getStopList()) {
this.stopListModel.addElement(stop);
}
this.stopList = new JList<>(this.stopListModel);
this.stopList.setCellRenderer(new StopRenderer());
this.stopList.setFixedCellHeight(48);
this.stopList.setDragEnabled(true);
this.stopList.setTransferHandler(new StopListHandler());
this.stopList.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_DELETE) {
deleteCurrentlySelectedStops();
}
}
@Override
public void keyPressed(KeyEvent e) {} // Ignore
@Override
public void keyReleased(KeyEvent e) {} // Ignore
});
this.stopList.addListSelectionListener(this);
JScrollPane tradeRouteView = new JScrollPane(stopList);
JLabel nameLabel
= Utility.localizedLabel("tradeRouteInputPanel.nameLabel");
this.tradeRouteName = new JTextField(tradeRoute.getName());
JLabel destinationLabel
= Utility.localizedLabel("tradeRouteInputPanel.destinationLabel");
this.destinationSelector = new JComboBox<>();
this.destinationSelector.setRenderer(new DestinationCellRenderer());
StringTemplate template
= StringTemplate.template("tradeRouteInputPanel.allColonies");
this.destinationSelector.addItem(Messages.message(template));
if (player.getEurope() != null) {
this.destinationSelector.addItem(player.getEurope().getId());
}
for (Colony colony : player.getColonyList()) {
this.destinationSelector.addItem(colony.getId());
}
this.messagesBox
= new JCheckBox(Messages.message("tradeRouteInputPanel.silence"));
this.messagesBox.setSelected(tradeRoute.isSilent());
this.messagesBox.addActionListener((ActionEvent ae) -> {
tradeRoute.setSilent(messagesBox.isSelected());
});
this.addStopButton
= Utility.localizedButton("tradeRouteInputPanel.addStop");
this.addStopButton.addActionListener((ActionEvent ae) -> {
addSelectedStops();
});
this.removeStopButton
= Utility.localizedButton("tradeRouteInputPanel.removeStop");
this.removeStopButton.addActionListener((ActionEvent ae) -> {
deleteCurrentlySelectedStops();
});
final List<GoodsType> gtl = getSpecification().getGoodsTypeList();
this.allGoodsTypesPanel = new AllGoodsTypesPanel(transform(gtl,
GoodsType::isStorable, gt -> buildCargoLabel(gt)));
this.allGoodsTypesPanel.setTransferHandler(this.transferHandler);
this.allGoodsTypesPanel.setEnabled(false);
this.stopGoodsTypesPanel = new StopGoodsTypesPanel();
this.stopGoodsTypesPanel.setTransferHandler(this.transferHandler);
JButton cancelButton = Utility.localizedButton("cancel");
cancelButton.setActionCommand(CANCEL);
cancelButton.addActionListener(this);
setCancelComponent(cancelButton);
add(Utility.localizedHeader("tradeRouteInputPanel.editRoute",
Utility.FONTSPEC_TITLE),
"span, align center");
add(tradeRouteView, "span 1 5, grow");
add(nameLabel);
add(this.tradeRouteName, "span");
add(destinationLabel);
add(this.destinationSelector, "span");
add(this.messagesBox);
add(this.addStopButton);
add(this.removeStopButton, "span");
add(this.allGoodsTypesPanel, "span");
add(this.stopGoodsTypesPanel, "span, height 80:, growy");
add(okButton, "newline 20, span, split 2, tag ok");
add(cancelButton, "tag cancel");
// update cargo panel if stop is selected
if (this.stopListModel.getSize() > 0) {
this.stopList.setSelectedIndex(0);
updateCargoPanel(this.stopListModel.firstElement());
}
// update buttons according to selection
updateButtons();
getGUI().restoreSavedSize(this, getPreferredSize());
}
/**
* Update the cargo panel to show a given stop.
*
* @param stop The {@code TradeRouteStop} to select.
*/
private void updateCargoPanel(TradeRouteStop stop) {
this.stopGoodsTypesPanel.setLabels((stop == null) ? null
: transform(stop.getCargo(), GoodsType::isStorable,
gt -> buildCargoLabel(gt)));
}
/**
* Import new goods at the selected stops.
*
* @param gt The {@code GoodsType} to import.
*/
private void enableImport(GoodsType gt) {
if (gt == null) return;
for (int stopIndex : this.stopList.getSelectedIndices()) {
TradeRouteStop stop = this.stopListModel.get(stopIndex);
stop.addCargo(gt);
}
this.stopList.revalidate();
this.stopList.repaint();
}
/**
* Cancel import of goods at the selected stops.
*
* @param gt The {@code GoodsType} to stop importing.
*/
private void cancelImport(GoodsType gt) {
this.stopGoodsTypesPanel.removeGoodsType(gt);
for (int stopIndex : this.stopList.getSelectedIndices()) {
TradeRouteStop stop = this.stopListModel.get(stopIndex);
List<GoodsType> cargo = new ArrayList<>(stop.getCargo());
if (removeInPlace(cargo, matchKey(gt))) {
stop.setCargo(cargo);
}
}
this.stopList.revalidate();
this.stopList.repaint();
this.stopGoodsTypesPanel.revalidate();
this.stopGoodsTypesPanel.repaint();
}
/**
* Add any stops selected in the destination selector.
*/
private void addSelectedStops() {
int startIndex = -1;
int endIndex = -1;
int sel = this.destinationSelector.getSelectedIndex();
if (sel == 0) { // All colonies + Europe
startIndex = 1;
endIndex = this.destinationSelector.getItemCount();
} else { // just one place
startIndex = sel;
endIndex = startIndex+1;
}
List<GoodsType> cargo
= transform(this.stopGoodsTypesPanel.getComponents(),
c -> c instanceof GoodsTypeLabel,
c -> ((GoodsTypeLabel)c).getType());
int maxIndex = this.stopList.getMaxSelectionIndex();
for (int i = startIndex; i < endIndex; i++) {
String id = this.destinationSelector.getItemAt(i);
FreeColGameObject fcgo = getGame().getFreeColGameObject(id);
if (fcgo instanceof Location) {
TradeRouteStop stop
= new TradeRouteStop(getGame(), (Location)fcgo);
stop.setCargo(cargo);
if (maxIndex < 0) {
this.stopListModel.addElement(stop);
} else {
maxIndex++;
this.stopListModel.add(maxIndex, stop);
}
}
}
}
/**
* Convenience function to build a new {@code GoodsTypeLabel}.
*
* @param gt The {@code GoodsType} for the label.
* @return A {@code GoodsTypeLabel} for the goods type.
*/
private GoodsTypeLabel buildCargoLabel(GoodsType gt) {
GoodsTypeLabel label = new GoodsTypeLabel(getFreeColClient(), gt);
label.setTransferHandler(this.transferHandler);
label.addMouseListener(this.dragListener);
return label;
}
/**
* Delete any stops currently selected in the stop list.
*/
private void deleteCurrentlySelectedStops() {
int count = 0;
int lastIndex = 0;
for (int index : this.stopList.getSelectedIndices()) {
this.stopListModel.remove(index - count);
count++;
lastIndex = index;
}
// If the remaining list is non-empty, make sure that the
// element beneath the last of the previously selected is
// selected (ie, delete one of many, the one -under- the
// deleted is selected) the user can then click in the list
// once, and continue deleting without having to click in the
// list again.
if (this.stopListModel.getSize() > 0) {
this.stopList.setSelectedIndex(lastIndex - count + 1);
}
}
/**
* Make sure the original route is invalid and remove this panel.
*
* Public so that this panel can be signalled to close if the parent
* TradeRoutePanel is closed.
*/
public void cancelTradeRoute() {
this.newRoute.setName(null);
getGUI().removeComponent(this);
}
/**
* Enables the remove stop button if a stop is selected and disables it
* otherwise.
*/
private void updateButtons() {
this.addStopButton.setEnabled(this.stopListModel.getSize()
< this.destinationSelector.getItemCount() - 1);
this.removeStopButton.setEnabled(this.stopList.getSelectedIndices()
.length > 0);
}
/**
* Check that the trade route is valid.
*
* @return True if the trade route is valid.
*/
private boolean verifyNewTradeRoute() {
// Update the trade route with the current settings
this.newRoute.setName(tradeRouteName.getText());
this.newRoute.clearStops();
for (int index = 0; index < this.stopListModel.getSize(); index++) {
this.newRoute.addStop(this.stopListModel.get(index));
}
this.newRoute.setSilent(this.messagesBox.isSelected());
StringTemplate err = this.newRoute.verify();
if (err != null) {
getGUI().showInformationPanel(err);
this.newRoute.setName(null); // Mark as unacceptable
return false;
}
return true;
}
// Interface ActionListener
/**
* {@inheritDoc}
*/
@Override
public void actionPerformed(ActionEvent ae) {
final String command = ae.getActionCommand();
if (command == null) return;
switch (command) {
case OK:
if (!verifyNewTradeRoute()) return;
// Return to TradeRoutePanel, which will add the route
// if needed, and it is valid.
super.actionPerformed(ae);
break;
case CANCEL:
cancelTradeRoute();
break;
default:
super.actionPerformed(ae);
break;
}
}
// Interface ListSelectionListener
/**
* {@inheritDoc}
*/
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) return;
int[] idx = this.stopList.getSelectedIndices();
if (idx.length > 0) {
updateCargoPanel(this.stopListModel.get(idx[0]));
this.allGoodsTypesPanel.setEnabled(true);
} else {
this.allGoodsTypesPanel.setEnabled(false);
}
updateButtons();
}
// Override Component
/**
* {@inheritDoc}
*/
@Override
public void removeNotify() {
this.transferHandler = null;
this.dragListener = null;
this.stopListModel.clear();
this.stopListModel = null;
this.stopList = null;
this.tradeRouteName = null;
this.destinationSelector = null;
this.messagesBox = null;
this.addStopButton = null;
this.removeStopButton = null;
this.stopGoodsTypesPanel = null;
this.allGoodsTypesPanel = null;
super.removeNotify();
}
}