package games.strategy.engine.framework.startup.ui;
import java.awt.Component;
import java.awt.FileDialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import games.strategy.engine.ClientContext;
import games.strategy.engine.ClientFileSystemHelper;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.GameParseException;
import games.strategy.engine.data.properties.IEditableProperty;
import games.strategy.engine.data.properties.PropertiesUI;
import games.strategy.engine.framework.map.download.DownloadMapsWindow;
import games.strategy.engine.framework.startup.mc.ClientModel;
import games.strategy.engine.framework.startup.mc.GameSelectorModel;
import games.strategy.engine.framework.system.SystemProperties;
import games.strategy.engine.framework.ui.NewGameChooser;
import games.strategy.engine.framework.ui.NewGameChooserEntry;
import games.strategy.engine.framework.ui.SaveGameFileChooser;
import games.strategy.ui.SwingComponents;
public class GameSelectorPanel extends JPanel implements Observer {
private static final long serialVersionUID = -4598107601238030020L;
private JLabel m_engineVersionLabel;
private JLabel m_engineVersionText;
private JLabel m_nameText;
private JLabel m_versionText;
private JLabel m_fileNameLabel;
private JLabel m_fileNameText;
private JLabel m_nameLabel;
private JLabel m_versionLabel;
private JLabel m_roundLabel;
private JLabel m_roundText;
private JButton m_loadSavedGame;
private JButton m_loadNewGame;
private JButton m_gameOptions;
private final GameSelectorModel m_model;
private final IGamePropertiesCache m_gamePropertiesCache = new FileBackedGamePropertiesCache();
private final Map<String, Object> m_originalPropertiesMap = new HashMap<>();
public GameSelectorPanel(final GameSelectorModel model) {
m_model = model;
m_model.addObserver(this);
final GameData data = model.getGameData();
if (data != null) {
setOriginalPropertiesMap(data);
m_gamePropertiesCache.loadCachedGamePropertiesInto(data);
}
createComponents();
layoutComponents();
setupListeners();
setWidgetActivation();
updateGameData();
}
private void updateGameData() {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> updateGameData());
return;
}
m_nameText.setText(m_model.getGameName());
m_versionText.setText(m_model.getGameVersion());
m_roundText.setText(m_model.getGameRound());
String fileName = m_model.getFileName();
if (fileName != null && fileName.length() > 1) {
try {
fileName = URLDecoder.decode(fileName, "UTF-8");
} catch (final IllegalArgumentException | UnsupportedEncodingException e) { // ignore
}
}
m_fileNameText.setText(getFormattedFileNameText(fileName,
Math.max(22, 3 + m_nameText.getText().length() + m_nameLabel.getText().length())));
m_fileNameText.setToolTipText(fileName);
}
/**
* Formats the file name text to two lines.
* The separation focuses on the second line being at least the filename while the first line
* should show the the path including '...' in case it does not fit
*
* @param fileName
* full file name
* @param maxLength
* maximum number of characters per line
* @return filename formatted file name - in case it is too long (> maxLength) to two lines
*/
private static String getFormattedFileNameText(final String fileName, final int maxLength) {
if (fileName.length() <= maxLength) {
return fileName;
}
int cutoff = fileName.length() - maxLength;
String secondLine = fileName.substring(cutoff);
if (secondLine.contains("/")) {
cutoff += secondLine.indexOf("/") + 1;
}
secondLine = fileName.substring(cutoff);
String firstLine = fileName.substring(0, cutoff);
if (firstLine.length() > maxLength) {
firstLine = firstLine.substring(0, maxLength - 4);
if (firstLine.contains("/")) {
cutoff = firstLine.lastIndexOf("/") + 1;
firstLine = firstLine.substring(0, cutoff) + ".../";
} else {
firstLine = firstLine + "...";
}
}
return "<html><p>" + firstLine + "<br/>" + secondLine + "</p></html>";
}
private void createComponents() {
m_engineVersionLabel = new JLabel("Engine Version:");
String version = ClientContext.engineVersion().getFullVersion();
m_engineVersionText = new JLabel(version);
m_nameLabel = new JLabel("Map Name:");
m_versionLabel = new JLabel("Map Version:");
m_roundLabel = new JLabel("Game Round:");
m_fileNameLabel = new JLabel("File Name:");
m_nameText = new JLabel();
m_versionText = new JLabel();
m_roundText = new JLabel();
m_fileNameText = new JLabel();
m_loadNewGame = new JButton("Select Map");
m_loadNewGame.setToolTipText("<html>Select a game from all the maps/games that come with TripleA, <br>and the ones "
+ "you have downloaded.</html>");
m_loadSavedGame = new JButton("Open Saved Game");
m_loadSavedGame.setToolTipText("Open a previously saved game, or an autosave.");
m_gameOptions = new JButton("Map Options");
m_gameOptions.setToolTipText("<html>Set options for the currently selected game, <br>such as enabling/disabling "
+ "Low Luck, or Technology, etc.</html>");
}
private void layoutComponents() {
setLayout(new GridBagLayout());
add(m_engineVersionLabel, buildGridCell(0, 0, new Insets(10, 10, 3, 5)));
add(m_engineVersionText, buildGridCell(1, 0, new Insets(10, 0, 3, 0)));
add(m_nameLabel, buildGridCell(0, 1, new Insets(0, 10, 3, 5)));
add(m_nameText, buildGridCell(1, 1, new Insets(0, 0, 3, 0)));
add(m_versionLabel, buildGridCell(0, 2, new Insets(0, 10, 3, 5)));
add(m_versionText, buildGridCell(1, 2, new Insets(0, 0, 3, 0)));
add(m_roundLabel, buildGridCell(0, 3, new Insets(0, 10, 3, 5)));
add(m_roundText, buildGridCell(1, 3, new Insets(0, 0, 3, 0)));
add(m_fileNameLabel, buildGridCell(0, 4, new Insets(20, 10, 3, 5)));
add(m_fileNameText, buildGridRow(0, 5, new Insets(0, 10, 3, 5)));
add(m_loadNewGame, buildGridRow(0, 6, new Insets(25, 10, 10, 10)));
add(m_loadSavedGame, buildGridRow(0, 7, new Insets(0, 10, 10, 10)));
JButton downloadMapButton =
SwingComponents.newJButton("Download Maps", "Click this button to install additional maps",
() -> DownloadMapsWindow.showDownloadMapsWindow());
add(downloadMapButton, buildGridRow(0, 8, new Insets(0, 10, 10, 10)));
add(m_gameOptions, buildGridRow(0, 9, new Insets(25, 10, 10, 10)));
// spacer
add(new JPanel(), new GridBagConstraints(0, 10, 2, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0));
}
private static GridBagConstraints buildGridCell(int x, int y, Insets insets) {
return buildGrid(x, y, insets, 1);
}
private static GridBagConstraints buildGridRow(int x, int y, Insets insets) {
return buildGrid(x, y, insets, 2);
}
private static GridBagConstraints buildGrid(int x, int y, Insets insets, int width) {
int gridWidth = width;
int gridHeight = 1;
double weigthX = 0;
double weigthY = 0;
int anchor = GridBagConstraints.WEST;
int fill = GridBagConstraints.NONE;
int ipadx = 0;
int ipady = 0;
return new GridBagConstraints(x, y, gridWidth, gridHeight, weigthX, weigthY, anchor, fill, insets, ipadx, ipady);
}
private void setupListeners() {
m_loadNewGame.addActionListener(e -> {
if (canSelectLocalGameData()) {
selectGameFile(false);
} else if (canChangeHostBotGameData()) {
final ClientModel clientModelForHostBots = m_model.getClientModelForHostBots();
if (clientModelForHostBots != null) {
clientModelForHostBots.getHostBotSetMapClientAction(GameSelectorPanel.this).actionPerformed(e);
}
}
});
m_loadSavedGame.addActionListener(e -> {
if (canSelectLocalGameData()) {
selectGameFile(true);
} else if (canChangeHostBotGameData()) {
final ClientModel clientModelForHostBots = m_model.getClientModelForHostBots();
if (clientModelForHostBots != null) {
final JPopupMenu menu = new JPopupMenu();
menu.add(clientModelForHostBots.getHostBotChangeGameToSaveGameClientAction(GameSelectorPanel.this));
menu.add(clientModelForHostBots.getHostBotChangeToAutosaveClientAction(GameSelectorPanel.this,
SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE));
menu.add(clientModelForHostBots.getHostBotChangeToAutosaveClientAction(GameSelectorPanel.this,
SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE2));
menu.add(clientModelForHostBots.getHostBotChangeToAutosaveClientAction(GameSelectorPanel.this,
SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE_ODD));
menu.add(clientModelForHostBots.getHostBotChangeToAutosaveClientAction(GameSelectorPanel.this,
SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE_EVEN));
menu.add(clientModelForHostBots.getHostBotGetGameSaveClientAction(GameSelectorPanel.this));
final Point point = m_loadSavedGame.getLocation();
menu.show(GameSelectorPanel.this, point.x + m_loadSavedGame.getWidth(), point.y);
}
}
});
m_gameOptions.addActionListener(e -> {
if (canSelectLocalGameData()) {
selectGameOptions();
} else if (canChangeHostBotGameData()) {
final ClientModel clientModelForHostBots = m_model.getClientModelForHostBots();
if (clientModelForHostBots != null) {
clientModelForHostBots.getHostBotChangeGameOptionsClientAction(GameSelectorPanel.this).actionPerformed(e);
}
}
});
}
private void setOriginalPropertiesMap(final GameData data) {
m_originalPropertiesMap.clear();
if (data != null) {
for (final IEditableProperty property : data.getProperties().getEditableProperties()) {
m_originalPropertiesMap.put(property.getName(), property.getValue());
}
}
}
private void selectGameOptions() {
// backup current game properties before showing dialog
final Map<String, Object> currentPropertiesMap = new HashMap<>();
for (final IEditableProperty property : m_model.getGameData().getProperties().getEditableProperties()) {
currentPropertiesMap.put(property.getName(), property.getValue());
}
final PropertiesUI panel = new PropertiesUI(m_model.getGameData().getProperties(), true);
final JScrollPane scroll = new JScrollPane(panel);
scroll.setBorder(null);
scroll.getViewport().setBorder(null);
final JOptionPane pane = new JOptionPane(scroll, JOptionPane.PLAIN_MESSAGE);
final String ok = "OK";
final String cancel = "Cancel";
final String makeDefault = "Make Default";
final String reset = "Reset";
pane.setOptions(new Object[] {ok, makeDefault, reset, cancel});
final JDialog window = pane.createDialog(JOptionPane.getFrameForComponent(this), "Map Options");
window.setVisible(true);
final Object buttonPressed = pane.getValue();
if (buttonPressed == null || buttonPressed.equals(cancel)) {
// restore properties, if cancel was pressed, or window was closed
final Iterator<IEditableProperty> itr = m_model.getGameData().getProperties().getEditableProperties().iterator();
while (itr.hasNext()) {
final IEditableProperty property = itr.next();
property.setValue(currentPropertiesMap.get(property.getName()));
}
} else if (buttonPressed.equals(reset)) {
if (!m_originalPropertiesMap.isEmpty()) {
// restore properties, if cancel was pressed, or window was closed
final Iterator<IEditableProperty> itr =
m_model.getGameData().getProperties().getEditableProperties().iterator();
while (itr.hasNext()) {
final IEditableProperty property = itr.next();
property.setValue(m_originalPropertiesMap.get(property.getName()));
}
selectGameOptions();
}
} else if (buttonPressed.equals(makeDefault)) {
m_gamePropertiesCache.cacheGameProperties(m_model.getGameData());
} else {
// ok was clicked, and we have modified the properties already
}
}
private void setWidgetActivation() {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> setWidgetActivation());
return;
}
final boolean canSelectGameData = canSelectLocalGameData();
final boolean canChangeHostBotGameData = canChangeHostBotGameData();
m_loadSavedGame.setEnabled(canSelectGameData || canChangeHostBotGameData);
m_loadNewGame.setEnabled(canSelectGameData || canChangeHostBotGameData);
// Disable game options if there are none.
if (canChangeHostBotGameData || (canSelectGameData && m_model.getGameData() != null
&& m_model.getGameData().getProperties().getEditableProperties().size() > 0)) {
m_gameOptions.setEnabled(true);
} else {
m_gameOptions.setEnabled(false);
}
// we don't want them starting new games if we are an old jar
if (ClientFileSystemHelper.areWeOldExtraJar()) {
m_loadNewGame.setEnabled(false);
m_loadNewGame.setToolTipText(
"This is disabled on older engine jars, please start new games with the latest version of TripleA.");
}
}
private boolean canSelectLocalGameData() {
return m_model != null && m_model.canSelect();
}
private boolean canChangeHostBotGameData() {
return m_model != null && m_model.isHostHeadlessBot();
}
@Override
public void update(final Observable o, final Object arg) {
updateGameData();
setWidgetActivation();
}
public static File selectGameFile(final Component parent) {
if (SystemProperties.isMac()) {
final FileDialog fileDialog = new FileDialog(JOptionPane.getFrameForComponent(parent));
fileDialog.setMode(FileDialog.LOAD);
SaveGameFileChooser.ensureMapsFolderExists();
fileDialog.setDirectory(new File(ClientContext.folderSettings().getSaveGamePath()).getPath());
fileDialog.setFilenameFilter((dir, name) -> {
// the extension should be .tsvg, but find svg extensions as well
// also, macs download the file as tsvg.gz, so accept that as well
return name.endsWith(".tsvg") || name.endsWith(".svg") || name.endsWith("tsvg.gz");
});
fileDialog.setVisible(true);
final String fileName = fileDialog.getFile();
final String dirName = fileDialog.getDirectory();
if (fileName == null) {
return null;
} else {
final File f = new File(dirName, fileName);
return f;
}
} else {
// Non-Mac platforms should use the normal Swing JFileChooser
final JFileChooser fileChooser = SaveGameFileChooser.getInstance();
final int rVal = fileChooser.showOpenDialog(JOptionPane.getFrameForComponent(parent));
if (rVal != JFileChooser.APPROVE_OPTION) {
return null;
}
return fileChooser.getSelectedFile();
}
}
private void selectGameFile(final boolean saved) {
// For some strange reason,
// the only way to get a Mac OS X native-style file dialog
// is to use an AWT FileDialog instead of a Swing JDialog
if (saved) {
final File file =
selectGameFile(SystemProperties.isMac() ? MainFrame.getInstance() : JOptionPane.getFrameForComponent(this));
if (file == null || !file.exists()) {
return;
}
m_model.load(file, this);
setOriginalPropertiesMap(m_model.getGameData());
} else {
final NewGameChooserEntry entry =
NewGameChooser.chooseGame(JOptionPane.getFrameForComponent(this), m_model.getGameName());
if (entry != null) {
if (!entry.isGameDataLoaded()) {
try {
entry.fullyParseGameData();
} catch (final GameParseException e) {
entry.delayParseGameData();
NewGameChooser.getNewGameChooserModel().removeEntry(entry);
return;
}
}
m_model.load(entry);
setOriginalPropertiesMap(m_model.getGameData());
// only for new games, not saved games, we set the default options, and set them only once (the first time it is
// loaded)
m_gamePropertiesCache.loadCachedGamePropertiesInto(m_model.getGameData());
}
}
}
}