diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index d6ccdfa2f27..9fc7cdeefc7 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -902,12 +902,14 @@ ClientGUI.BotCommand=Bot Command ClientGUI.clientTitleSuffix=\ - MegaMek ClientGUI.dialogMovementReport=Movement Report ClientGUI.dialogTacticalGeniusReport=Tactical Genius Report +ClientGUI.descriptionGIFFiles=GIF (Graphics Interchange Format) ClientGUI.descriptionMULFiles=Mul Files ClientGUI.Disconnected.message=You have become disconnected from the server. ClientGUI.Disconnected.title=Disconnected\! ClientGUI.distance=distance: ClientGUI.errorLoadingFile=Error Loading File ClientGUI.errorSavingFile=Error Saving File +ClientGUI.errorSavingFileGifMessage=MegaMek was unable to save {0} ClientGUI.errorSelectingPlayer=Error selecting player ClientGUI.failedToLoadAudioFile=Failed to load audio file named ClientGUI.FatalError.message=Could not initialise:\n @@ -933,8 +935,11 @@ ClientGUI.openUnitListFileDialog.noReinforceTitle=Must have a team assigned! ClientGUI.openUnitListFileDialog.noReinforceMessage=Players must have an assigned team in order to reinforce units!\nObservers may join a team using the /joinTeam command.\nFor more information, use /help joinTeam. ClientGUI.openUnitListFileDialog.title=Open Unit List File ClientGUI.saveUnitListFileDialog.title=Save Unit List As +ClientGUI.saveGameSummaryGifFileDialog.title=Save Game Summary GIF As ClientGUI.SaveUnitsDialog.message=Do you want to save a record of all units\n(including salvage) to a file? ClientGUI.SaveUnitsDialog.title=Save Units? +ClientGUI.SaveGifDialog.title=Save GIF? +ClientGUI.SaveGifDialog.message=Do you want to save a GIF of the game summary? ClientGUI.selectMenuItem=Select\u0020 ClientGUI.skinningHelpPath=docs/help/en/skinning/skinningHowTo.html ClientGUI.skinningHelpPath.title=How To: Skinning @@ -1284,6 +1289,8 @@ CommonSettingsDialog.showUnitDisplayNamesOnMinimap.name=Show unit display name o CommonSettingsDialog.showUnitDisplayNamesOnMinimap.tooltip=Useful when you want to persist the game summary with the unit names. Helps to navigate the targets at a glance on a large map. CommonSettingsDialog.gameSummaryMM.name=Save game summary image of minimap CommonSettingsDialog.gameSummaryMM.tooltip=At the end of certain phases, save an image of the minimap to {0}, creating a visual summary of the progression of the game. +CommonSettingsDialog.gifGameSummaryMM.name=Save game summary GIF of minimap +CommonSettingsDialog.gifGameSummaryMM.tooltip=At the end of certain phases, a still image of the minimap will be turned into a frame of a GIF, which will be saved to {0}. CommonSettingsDialog.generateNames=Generate a random name for the pilots of new units. CommonSettingsDialog.getFocus=Get Focus when a new phase begins. CommonSettingsDialog.guiScale=GUI Scale diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index 5cf420c2f46..17e08c14424 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -99,6 +99,8 @@ import megamek.logging.MMLogger; import org.apache.commons.lang3.SystemUtils; +import static megamek.common.Configuration.gameSummaryImagesMMDir; + public class ClientGUI extends AbstractClientGUI implements BoardViewListener, ActionListener, IPreferenceChangeListener, MekDisplayListener, ILocalBots, IDisconnectSilently, IHasUnitDisplay, IHasBoardView, IHasMenuBar, IHasCurrentPanel { private final static MMLogger logger = MMLogger.create(ClientGUI.class); @@ -235,6 +237,8 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, public static final String CG_FILEEXTENTIONMUL = ".mul"; public static final String CG_FILEEXTENTIONXML = ".xml"; public static final String CG_FILEEXTENTIONPNG = ".png"; + public static final String CG_FILEEXTENTIONGIF = ".gif"; + public static final String CG_FILEPATHGIF = "gif"; public static final String CG_FILEFORMATNAMEPNG = "png"; // a frame, to show stuff in @@ -290,6 +294,7 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, */ private JFileChooser dlgLoadList; private JFileChooser dlgSaveList; + private JFileChooser dlgSaveGifList; private final Client client; private File curfileBoardImage; @@ -2255,6 +2260,68 @@ public void printList(ArrayList unitList, JButton button) { } } + private void saveGifGameSummary() { + String filename = StringUtil.addDateTimeStamp(client.getLocalPlayer().getName()); + String gameUuid = client.getGame().getUUIDString(); + File tempGifFile = new File(gameSummaryImagesMMDir(), gameUuid + CG_FILEEXTENTIONGIF); + + // Build the "save gif" dialog, if necessary. + if (dlgSaveGifList == null) { + dlgSaveGifList = new JFileChooser("."); + dlgSaveGifList.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100); + dlgSaveGifList.setDialogTitle(Messages.getString("ClientGUI.saveGameSummaryGifFileDialog.title")); + FileNameExtensionFilter filter = new FileNameExtensionFilter( + Messages.getString("ClientGUI.descriptionGIFFiles"), CG_FILEPATHGIF); + dlgSaveGifList.setFileFilter(filter); + } + + dlgSaveGifList.setSelectedFile(new File(filename + CG_FILEEXTENTIONGIF)); + + int returnVal = dlgSaveGifList.showSaveDialog(frame); + if ((returnVal != JFileChooser.APPROVE_OPTION) || (dlgSaveGifList.getSelectedFile() == null)) { + // without a file there is no saving for the file, which them means we can't save the gif + // and instead we delete it + if (tempGifFile.delete()) { + logger.info("Game summary GIF deleted"); + } else { + logger.error("Failed to delete game summary GIF"); + } + return; + } + + // Did the player select a file? + File gifFile = dlgSaveGifList.getSelectedFile(); + if (gifFile != null) { + if (!gifFile.getName().toLowerCase().endsWith(CG_FILEEXTENTIONGIF)) { + try { + gifFile = new File(gifFile.getCanonicalPath() + CG_FILEEXTENTIONGIF); + } catch (Exception ignored) { + // without a file there is no saving for the file, which them means we can't save the gif + // and instead we delete it + if (tempGifFile.delete()) { + logger.info("Game summary GIF deleted"); + } else { + logger.error("Failed to delete game summary GIF"); + } + return; + } + } + + try { + if (tempGifFile.renameTo(gifFile)) { + logger.info("Game summary GIF saved to {}", gifFile); + } else { + logger.error("Failed to save game summary GIF to {}", gifFile); + doAlertDialog(Messages.getString("ClientGUI.errorSavingFile"), + Messages.getString("ClientGUI.errorSavingFileGifMessage", gifFile.toString())); + } + } catch (Exception ex) { + logger.error(ex, "saveVictoryList"); + doAlertDialog(Messages.getString("ClientGUI.errorSavingFile"), ex.getMessage()); + } + } + } + protected void saveVictoryList() { String filename = client.getLocalPlayer().getName(); @@ -2475,6 +2542,23 @@ public void gameEnd(GameEndEvent e) { } } + if (GUIP.getGifGameSummaryMinimap()) { + // Ask if you want to persist the final unit list from a battle encounter + if (doYesNoDialog(Messages.getString("ClientGUI.SaveGifDialog.title"), + Messages.getString("ClientGUI.SaveGifDialog.message"))) { + saveGifGameSummary(); + } else { + String gameUuid = client.getGame().getUUIDString(); + File tempGifFile = new File(gameSummaryImagesMMDir(), gameUuid + CG_FILEEXTENTIONGIF); + if (tempGifFile.delete()) { + logger.info("Deleted temporary game summary GIF {}", tempGifFile); + } else { + logger.error("Failed to delete temporary game summary GIF {}", tempGifFile); + } + } + } + + // save all destroyed units in a separate "salvage MUL" ArrayList destroyed = new ArrayList<>(); Enumeration graveyard = getClient().getGame().getGraveyardEntities(); diff --git a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java index 0e1c8bb5740..5c60d54ffda 100644 --- a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java +++ b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java @@ -20,38 +20,7 @@ */ package megamek.client.ui.swing; -import static java.util.stream.Collectors.toList; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Container; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Stream; - -import javax.swing.*; -import javax.swing.UIManager.LookAndFeelInfo; -import javax.swing.border.EmptyBorder; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.event.MouseInputAdapter; - import com.formdev.flatlaf.icons.FlatHelpButtonIcon; - import megamek.MMConstants; import megamek.client.ui.Messages; import megamek.client.ui.baseComponents.AbstractButtonDialog; @@ -76,6 +45,25 @@ import megamek.common.util.fileUtils.MegaMekFile; import megamek.logging.MMLogger; +import javax.swing.*; +import javax.swing.UIManager.LookAndFeelInfo; +import javax.swing.border.EmptyBorder; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.*; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; + /** * The Client Settings Dialog offering GUI options concerning tooltips, map * display, keybinds etc. @@ -349,6 +337,8 @@ private void moveElement(DefaultListModel srcModel, int srcIndex, int trg Messages.getString("CommonSettingsDialog.gameSummaryBV.name")); private final JCheckBox gameSummaryMM = new JCheckBox( Messages.getString("CommonSettingsDialog.gameSummaryMM.name")); + private final JCheckBox gifGameSummaryMM = new JCheckBox( + Messages.getString("CommonSettingsDialog.gifGameSummaryMM.name")); private final JCheckBox showUnitDisplayNamesOnMinimap = new JCheckBox( Messages.getString("CommonSettingsDialog.showUnitDisplayNamesOnMinimap.name")); private JComboBox skinFiles; @@ -1674,6 +1664,9 @@ private JPanel getMiniMapPanel() { comps.add(checkboxEntry(gameSummaryMM, Messages.getString("CommonSettingsDialog.gameSummaryMM.tooltip", Configuration.gameSummaryImagesMMDir()))); + comps.add(checkboxEntry(gifGameSummaryMM, + Messages.getString("CommonSettingsDialog.gifGameSummaryMM.tooltip", + Configuration.gameSummaryImagesMMDir()))); comps.add(checkboxEntry(drawFacingArrowsOnMiniMap, null)); comps.add(checkboxEntry(drawSensorRangeOnMiniMap, null)); comps.add(checkboxEntry(paintBordersOnMiniMap, null)); @@ -2114,7 +2107,7 @@ public void setVisible(boolean visible) { gameSummaryBV.setSelected(GUIP.getGameSummaryBoardView()); gameSummaryMM.setSelected(GUIP.getGameSummaryMinimap()); - + gifGameSummaryMM.setSelected(GUIP.getGifGameSummaryMinimap()); skinFiles.removeAllItems(); ArrayList xmlFiles = new ArrayList<>(filteredFiles(Configuration.skinsDir(), ".xml")); @@ -2584,10 +2577,11 @@ protected void okAction() { GUIP.setAutoSelectNextUnit(useAutoSelectNext.isSelected()); GUIP.setGameSummaryBoardView(gameSummaryBV.isSelected()); GUIP.setGameSummaryMinimap(gameSummaryMM.isSelected()); + GUIP.setGifGameSummaryMinimap(gifGameSummaryMM.isSelected()); GUIP.setShowUnitDisplayNamesOnMinimap(showUnitDisplayNamesOnMinimap.isSelected()); UITheme newUITheme = (UITheme) uiThemes.getSelectedItem(); String oldUITheme = GUIP.getUITheme(); - if (!oldUITheme.equals(newUITheme.getClassName())) { + if (newUITheme != null && !oldUITheme.equals(newUITheme.getClassName())) { GUIP.setUITheme(newUITheme.getClassName()); } diff --git a/megamek/src/megamek/client/ui/swing/GUIPreferences.java b/megamek/src/megamek/client/ui/swing/GUIPreferences.java index 834223c00da..f250a5db589 100644 --- a/megamek/src/megamek/client/ui/swing/GUIPreferences.java +++ b/megamek/src/megamek/client/ui/swing/GUIPreferences.java @@ -214,6 +214,7 @@ public class GUIPreferences extends PreferenceStoreProxy { public static final String SPLIT_PANE_A_DIVIDER_LOCATION = "SplitPaneADividerLocation"; public static final String GAME_SUMMARY_BOARD_VIEW = "GameSummaryBoardView"; public static final String GAME_SUMMARY_MINIMAP = "GameSummaryMinimap"; + public static final String GIF_GAME_SUMMARY_MINIMAP = "GifGameSummaryMinimap"; public static final String SHOW_UNIT_DISPLAY_NAMES_ON_MINIMAP = "ShowUnitDisplayNamesOnMinimap"; public static final String ENTITY_OWNER_LABEL_COLOR = "EntityOwnerLabelColor"; public static final String UNIT_LABEL_BORDER = "EntityOwnerLabelColor"; @@ -689,6 +690,8 @@ protected GUIPreferences() { store.setDefault(MINI_MAP_SHOW_FACING_ARROW, true); store.setDefault(MINI_MAP_PAINT_BORDERS, true); store.setDefault(MINI_MAP_MOVE_PATH_PERSISTENCE, 2); + store.setDefault(GIF_GAME_SUMMARY_MINIMAP, true); + store.setDefault(GAME_SUMMARY_MINIMAP, false); store.setDefault(SHOW_UNIT_DISPLAY_NAMES_ON_MINIMAP, false); store.setDefault(MOVE_DISPLAY_TAB_DURING_PHASES, true); store.setDefault(FIRE_DISPLAY_TAB_DURING_PHASES, true); @@ -1078,6 +1081,10 @@ public boolean getGameSummaryMinimap() { return store.getBoolean(GAME_SUMMARY_MINIMAP); } + public boolean getGifGameSummaryMinimap() { + return store.getBoolean(GIF_GAME_SUMMARY_MINIMAP); + } + public boolean showUnitDisplayNamesOnMinimap() { return store.getBoolean(SHOW_UNIT_DISPLAY_NAMES_ON_MINIMAP); } @@ -1927,6 +1934,10 @@ public void setGameSummaryMinimap(boolean state) { store.setValue(GAME_SUMMARY_MINIMAP, state); } + public void setGifGameSummaryMinimap(boolean state) { + store.setValue(GIF_GAME_SUMMARY_MINIMAP, state); + } + public void setShowUnitDisplayNamesOnMinimap(boolean state) { store.setValue(SHOW_UNIT_DISPLAY_NAMES_ON_MINIMAP, state); } diff --git a/megamek/src/megamek/client/ui/swing/minimap/Minimap.java b/megamek/src/megamek/client/ui/swing/minimap/Minimap.java index 5954e0293e5..fd7cd05deab 100644 --- a/megamek/src/megamek/client/ui/swing/minimap/Minimap.java +++ b/megamek/src/megamek/client/ui/swing/minimap/Minimap.java @@ -120,6 +120,7 @@ public final class Minimap extends JPanel implements IPreferenceChangeListener { private static final int MARGIN = 3; private static final int BUTTON_HEIGHT = 14; + /** * The minimap zoom at which game summary images are saved regardless of the * ingame minimap setting. @@ -303,7 +304,7 @@ private void initializeListeners() { @Override public void gamePhaseChange(GamePhaseChangeEvent e) { - if (GUIP.getGameSummaryMinimap() + if ((GUIP.getGameSummaryMinimap() || GUIP.getGifGameSummaryMinimap()) && (e.getOldPhase().isDeployment() || e.getOldPhase().isMovement() || e.getOldPhase().isTargeting() || e.getOldPhase().isPremovement() || e.getOldPhase().isPrefiring() || e.getOldPhase().isFiring() @@ -315,20 +316,23 @@ public void gamePhaseChange(GamePhaseChangeEvent e) { } File imgFile = new File(dir, "round_" + game.getRoundCount() + "_" + e.getOldPhase().ordinal() + "_" + e.getOldPhase() + ".png"); - if (gifWriterThread == null) { - gifWriterThread = new GifWriterThread(new GifWriter(game.getUUIDString()), "GifWriterThread"); - gifWriterThread.start(); - } - try { + try { BufferedImage image = getMinimapImage(game, bv, GAME_SUMMARY_ZOOM, clientGui, null, movePathLines); - ImageIO.write(image, "png", imgFile); - long frameDurationInMillis = e.getOldPhase().isFiring()? 400 : 200; - gifWriterThread.addFrame(image, frameDurationInMillis); + if (GUIP.getGameSummaryMinimap()) { + ImageIO.write(image, "png", imgFile); + } + if (GUIP.getGifGameSummaryMinimap()) { + if (gifWriterThread == null) { + gifWriterThread = new GifWriterThread(new GifWriter(game.getUUIDString()), "GifWriterThread"); + gifWriterThread.start(); + } + gifWriterThread.addFrame(image, 400); + } } catch (Exception ex) { logger.error(ex, "Error saving game summary image."); } - if (e.getNewPhase().isVictory() && gifWriterThread.isAlive()) { + if (e.getNewPhase().isVictory() && (gifWriterThread != null) && gifWriterThread.isAlive()) { try { gifWriterThread.stopThread(); } catch (Exception ex) {