package games.strategy.engine.framework.map.download;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.swing.Box;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionListener;
import games.strategy.engine.ClientContext;
import games.strategy.engine.framework.GameRunner;
import games.strategy.engine.framework.ui.background.BackgroundTaskRunner;
import games.strategy.ui.SwingComponents;
import games.strategy.util.Version;
/** Window that allows for map downloads and removal. */
public class DownloadMapsWindow extends JFrame {
private enum MapAction {
INSTALL, UPDATE, REMOVE
}
private static final long serialVersionUID = -1542210716764178580L;
private static final int WINDOW_WIDTH = 1200;
private static final int WINDOW_HEIGHT = 700;
private static final int DIVIDER_POSITION = WINDOW_HEIGHT - 150;
private final MapDownloadProgressPanel progressPanel;
public static Version getVersion(final File zipFile) {
final DownloadFileProperties props = DownloadFileProperties.loadForZip(zipFile);
return props.getVersion();
}
/**
* Shows the download window and begins downloading the specified map right away.
* If the map cannot be downloaded a message prompt is shown to the user.
*/
public static void showDownloadMapsWindow(final String mapName) {
showDownloadMapsWindow(Optional.of(mapName));
}
public static void showDownloadMapsWindow() {
showDownloadMapsWindow(Optional.empty());
}
private static void showDownloadMapsWindow(Optional<String> mapName) {
Runnable downloadAndShowWindow = () -> {
final List<DownloadFileDescription> games =
new DownloadRunnable(ClientContext.mapListingSource().getMapListDownloadSite()).getDownloads();
checkNotNull(games);
SwingUtilities.invokeLater(() -> {
final DownloadMapsWindow dia = new DownloadMapsWindow(mapName, games);
dia.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
dia.setLocationRelativeTo(null);
dia.setMinimumSize(new Dimension(200, 200));
dia.setVisible(true);
dia.requestFocus();
dia.toFront();
});
};
final String popupWindowTitle = "Downloading list of availabe maps....";
BackgroundTaskRunner.runInBackground(popupWindowTitle, downloadAndShowWindow);
}
private DownloadMapsWindow(final Optional<String> mapName, final List<DownloadFileDescription> games) {
super("Download Maps");
setIconImage(GameRunner.getGameIcon(this));
progressPanel = new MapDownloadProgressPanel(this);
if (mapName.isPresent()) {
final Optional<DownloadFileDescription> mapDownload = findMap(mapName.get(), games);
if (mapDownload.isPresent()) {
progressPanel.download(Arrays.asList(mapDownload.get()));
} else {
SwingComponents.newMessageDialog("Unable to download map, could not find: " + mapName.get());
}
}
SwingComponents.addWindowCloseListener(this, () -> progressPanel.cancel());
final JTabbedPane outerTabs = new JTabbedPane();
final List<DownloadFileDescription> maps = filterMaps(games, download -> download.isMap());
outerTabs.add("Maps", createdTabbedPanelForMaps(maps));
final List<DownloadFileDescription> skins = filterMaps(games, download -> download.isMapSkin());
outerTabs.add("Skins", createAvailableInstalledTabbedPanel(mapName, skins));
final List<DownloadFileDescription> tools = filterMaps(games, download -> download.isMapTool());
outerTabs.add("Tools", createAvailableInstalledTabbedPanel(mapName, tools));
final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, outerTabs,
SwingComponents.newJScrollPane(progressPanel));
splitPane.setDividerLocation(DIVIDER_POSITION);
add(splitPane);
}
private Component createdTabbedPanelForMaps(List<DownloadFileDescription> maps) {
JTabbedPane mapTabs = SwingComponents.newJTabbedPane();
for (DownloadFileDescription.MapCategory mapCategory : Arrays
.asList(DownloadFileDescription.MapCategory.values())) {
List<DownloadFileDescription> mapsByCategory =
maps.stream().filter(map -> map.getMapCategory() == mapCategory).collect(Collectors.toList());
if (!mapsByCategory.isEmpty()) {
JTabbedPane subTab = createAvailableInstalledTabbedPanel(Optional.of(mapCategory.toString()), mapsByCategory);
mapTabs.add(mapCategory.toString(), subTab);
}
}
return mapTabs;
}
private static Optional<DownloadFileDescription> findMap(final String mapName,
final List<DownloadFileDescription> games) {
final String normalizedName = normalizeName(mapName);
for (final DownloadFileDescription download : games) {
if (download.getMapName().equalsIgnoreCase(mapName)
|| normalizedName.equals(normalizeName(download.getMapName()))) {
return Optional.of(download);
}
}
return Optional.empty();
}
private static String normalizeName(final String mapName) {
return mapName.replace(' ', '_').toLowerCase();
}
private static List<DownloadFileDescription> filterMaps(final List<DownloadFileDescription> maps,
final Function<DownloadFileDescription, Boolean> filter) {
maps.forEach(map -> checkNotNull("Maps list contained null element: " + maps, map));
return maps.stream().filter(map -> filter.apply(map)).collect(Collectors.toList());
}
private JTabbedPane createAvailableInstalledTabbedPanel(final Optional<String> mapName,
final List<DownloadFileDescription> games) {
final MapDownloadList mapList = new MapDownloadList(games, new FileSystemAccessStrategy());
final JTabbedPane tabbedPane = new JTabbedPane();
JPanel outOfDate = null;
if (!mapList.getOutOfDate().isEmpty()) {
outOfDate = createMapSelectionPanel(mapName, mapList.getOutOfDate(), MapAction.UPDATE);
}
// For the UX, always show an available maps tab, even if it is empty
final JPanel available = createMapSelectionPanel(mapName, mapList.getAvailable(), MapAction.INSTALL);
// if there is a map to preselect, show the available map list first
if (mapName.isPresent()) {
tabbedPane.addTab("Available", available);
}
// otherwise show the updates first
if (outOfDate != null) {
tabbedPane.addTab("Update", outOfDate);
}
// finally make sure we are always showing the 'available' tab, this condition will be
// true if the first 'mapName.isPresent()' is false
if (!mapName.isPresent()) {
tabbedPane.addTab("Available", available);
}
if (!mapList.getInstalled().isEmpty()) {
final JPanel installed = createMapSelectionPanel(mapName, mapList.getInstalled(), MapAction.REMOVE);
tabbedPane.addTab("Installed", installed);
}
return tabbedPane;
}
private JPanel createMapSelectionPanel(final Optional<String> selectedMap,
final List<DownloadFileDescription> unsortedMaps, final MapAction action) {
final List<DownloadFileDescription> maps = MapDownloadListSort.sortByMapName(unsortedMaps);
final JPanel main = SwingComponents.newBorderedPanel(30);
final JEditorPane descriptionPane = SwingComponents.newHtmlJEditorPane();
main.add(SwingComponents.newJScrollPane(descriptionPane), BorderLayout.CENTER);
final JLabel mapSizeLabel = new JLabel(" ");
final DefaultListModel<String> model = SwingComponents.newJListModel(maps, map -> map.getMapName());
if (maps.size() > 0) {
final DownloadFileDescription mapToSelect = determineCurrentMapSelection(maps, selectedMap);
final JList<String> gamesList = createGameSelectionList(mapToSelect, maps, descriptionPane, model);
gamesList.addListSelectionListener(createDescriptionPanelUpdatingSelectionListener(
descriptionPane, gamesList, maps, action, mapSizeLabel));
DownloadMapsWindow.updateMapUrlAndSizeLabel(mapToSelect, action, mapSizeLabel);
main.add(SwingComponents.newJScrollPane(gamesList), BorderLayout.WEST);
final JPanel southPanel = SwingComponents.gridPanel(2, 1);
southPanel.add(mapSizeLabel);
southPanel.add(createButtonsPanel(action, gamesList, maps, model));
main.add(southPanel, BorderLayout.SOUTH);
}
return main;
}
private DownloadFileDescription determineCurrentMapSelection(final List<DownloadFileDescription> maps,
final Optional<String> mapToSelect) {
checkArgument(maps.size() > 0);
if (mapToSelect.isPresent()) {
final Optional<DownloadFileDescription> potentialMap =
maps.stream().filter(m -> m.getMapName().equalsIgnoreCase(mapToSelect.get())).findFirst();
if (potentialMap.isPresent()) {
return potentialMap.get();
}
}
// just return the first map if nothing selected or could not find one
return maps.get(0);
}
private static JList<String> createGameSelectionList(final DownloadFileDescription selectedMap,
final List<DownloadFileDescription> maps, final JEditorPane descriptionPanel,
final DefaultListModel<String> model) {
final JList<String> gamesList = SwingComponents.newJList(model);
final int selectedIndex = maps.indexOf(selectedMap);
gamesList.setSelectedIndex(selectedIndex);
final String text = maps.get(selectedIndex).toHtmlString();
descriptionPanel.setText(text);
descriptionPanel.scrollRectToVisible(new Rectangle(0, 0, 0, 0));
return gamesList;
}
private static ListSelectionListener createDescriptionPanelUpdatingSelectionListener(
final JEditorPane descriptionPanel,
final JList<String> gamesList, final List<DownloadFileDescription> maps, final MapAction action,
final JLabel mapSizeLabelToUpdate) {
return e -> {
final int index = gamesList.getSelectedIndex();
final boolean somethingIsSelected = index >= 0;
if (somethingIsSelected) {
final String mapName = gamesList.getModel().getElementAt(index);
// find the map description by map name and update the map download detail panel
final Optional<DownloadFileDescription> map =
maps.stream().filter(mapDescription -> mapDescription.getMapName().equals(mapName)).findFirst();
if (map.isPresent()) {
final String text = map.get().toHtmlString();
descriptionPanel.setText(text);
descriptionPanel.scrollRectToVisible(new Rectangle(0, 0, 0, 0));
updateMapUrlAndSizeLabel(map.get(), action, mapSizeLabelToUpdate);
}
}
};
}
private static void updateMapUrlAndSizeLabel(final DownloadFileDescription map, final MapAction action,
final JLabel mapSizeLabel) {
mapSizeLabel.setText(" ");
new Thread(() -> {
final String labelText = createLabelText(action, map);
SwingUtilities.invokeLater(() -> mapSizeLabel.setText(labelText));
}).start();
}
private static String createLabelText(final MapAction action, final DownloadFileDescription map) {
final String DOUBLE_SPACE = " ";
final long mapSize;
String labelText = "<html>" + map.getMapName() + DOUBLE_SPACE + " v" + map.getVersion();
if (action == MapAction.INSTALL) {
if (map.newURL() == null) {
mapSize = 0L;
} else {
mapSize = DownloadUtils.getDownloadLength(map.newURL()).orElse(-1);
}
} else {
mapSize = map.getInstallLocation().length();
}
if (mapSize > 0) {
labelText += DOUBLE_SPACE + " (" + createSizeLabel(mapSize) + ")";
}
labelText += "<br/>";
if (action == MapAction.INSTALL) {
labelText += map.getUrl();
} else {
labelText += map.getInstallLocation().getAbsolutePath();
}
labelText += "</html>";
return labelText;
}
private static String createSizeLabel(final long bytes) {
final long kiloBytes = (bytes / 1024);
final long megaBytes = kiloBytes / 1024;
final long kbDigits = ((kiloBytes % 1000) / 100);
return megaBytes + "." + kbDigits + " MB";
}
private JPanel createButtonsPanel(final MapAction action, final JList<String> gamesList,
final List<DownloadFileDescription> maps,
final DefaultListModel<String> listModel) {
final JPanel buttonsPanel = SwingComponents.gridPanel(1, 5);
buttonsPanel.setBorder(SwingComponents.newEmptyBorder(20));
buttonsPanel.add(buildMapActionButton(action, gamesList, maps, listModel));
buttonsPanel.add(Box.createGlue());
String toolTip = "Click this button to learn more about the map download feature in TripleA";
final JButton helpButton = SwingComponents.newJButton("Help", toolTip,
e -> JOptionPane.showMessageDialog(this, new MapDownloadHelpPanel()));
buttonsPanel.add(helpButton);
toolTip = "Click this button to submit map comments and bug reports back to the map makers";
final JButton mapFeedbackButton = SwingComponents.newJButton("Give Map Feedback", toolTip,
e -> FeedbackDialog.showFeedbackDialog(gamesList.getSelectedValuesList(), maps));
buttonsPanel.add(mapFeedbackButton);
buttonsPanel.add(Box.createGlue());
toolTip = "Click this button to close the map download window and cancel any in-progress downloads.";
final JButton closeButton = SwingComponents.newJButton("Close", toolTip, e -> {
setVisible(false);
dispose();
});
buttonsPanel.add(closeButton);
return buttonsPanel;
}
private static final String MULTIPLE_SELECT_MSG =
"You can select multiple maps by holding control or shift while clicking map names.";
private JButton buildMapActionButton(final MapAction action, final JList<String> gamesList,
final List<DownloadFileDescription> maps,
final DefaultListModel<String> listModel) {
final JButton actionButton;
if (action == MapAction.REMOVE) {
actionButton = SwingComponents.newJButton("Remove", removeAction(gamesList, maps, listModel));
final String hoverText =
"Click this button to remove the maps selected above from your computer. " + MULTIPLE_SELECT_MSG;
actionButton.setToolTipText(hoverText);
} else {
final String buttonText = (action == MapAction.INSTALL) ? "Install" : "Update";
actionButton = SwingComponents.newJButton(buttonText, installAction(gamesList, maps, listModel));
final String hoverText =
"Click this button to download and install the maps selected above. " + MULTIPLE_SELECT_MSG;
actionButton.setToolTipText(hoverText);
}
return actionButton;
}
private static ActionListener removeAction(final JList<String> gamesList, final List<DownloadFileDescription> maps,
final DefaultListModel<String> listModel) {
return (e) -> {
final List<String> selectedValues = gamesList.getSelectedValuesList();
final List<DownloadFileDescription> selectedMaps =
maps.stream().filter(map -> selectedValues.contains(map.getMapName()))
.collect(Collectors.toList());
if (!selectedMaps.isEmpty()) {
FileSystemAccessStrategy.remove(selectedMaps, listModel);
}
};
}
private ActionListener installAction(final JList<String> gamesList, final List<DownloadFileDescription> maps,
final DefaultListModel<String> listModel) {
return (e) -> {
final List<String> selectedValues = gamesList.getSelectedValuesList();
final List<DownloadFileDescription> downloadList = new ArrayList<>();
for (final DownloadFileDescription map : maps) {
if (selectedValues.contains(map.getMapName())) {
downloadList.add(map);
}
}
if (!downloadList.isEmpty()) {
progressPanel.download(downloadList);
}
downloadList.forEach(m -> listModel.removeElement(m.getMapName()));
};
}
}