Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display progress indicator while exporting screenshot #1419

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/games/strategy/triplea/ui/MapPanel.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package games.strategy.triplea.ui;

import static com.google.common.base.Preconditions.checkNotNull;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Cursor;
Expand Down Expand Up @@ -461,11 +463,17 @@ public void setTopLeft(final int x, final int y) {
super.setTopLeft(x, y);
}

// this one is useful for screenshots
@Override
public void print(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
super.print(g2d);
/**
* Draws an image of the complete map to the specified graphics context.
*
* <p>
* This method is useful for capturing screenshots. This method can be called from a thread other than the EDT.
* </p>
*
* @param g The graphics context on which to draw the map; must not be {@code null}.
*/
public void drawMapImage(final Graphics g) {
final Graphics2D g2d = (Graphics2D) checkNotNull(g);
// make sure we use the same data for the entire print
final GameData gameData = m_data;
final Rectangle2D.Double bounds = new Rectangle2D.Double(0, 0, getImageWidth(), getImageHeight());
Expand Down
4 changes: 2 additions & 2 deletions src/games/strategy/triplea/ui/TripleAFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.image.TileImageFactory;
import games.strategy.triplea.settings.scrolling.ScrollSettings;
import games.strategy.triplea.ui.export.ScreenshotExporter;
import games.strategy.triplea.ui.history.HistoryDetailsPanel;
import games.strategy.triplea.ui.history.HistoryLog;
import games.strategy.triplea.ui.history.HistoryPanel;
import games.strategy.triplea.ui.menubar.ExportMenu;
import games.strategy.triplea.ui.menubar.HelpMenu;
import games.strategy.triplea.ui.menubar.TripleAMenuBar;
import games.strategy.triplea.ui.screen.UnitsDrawer;
Expand Down Expand Up @@ -1735,7 +1735,7 @@ public void actionPerformed(final ActionEvent ae) {

@Override
public void actionPerformed(final ActionEvent ae) {
ExportMenu.saveScreenshot(historyPanel.getCurrentPopupNode(), TripleAFrame.this, data);
ScreenshotExporter.exportScreenshot(TripleAFrame.this, data, historyPanel.getCurrentPopupNode());
historyPanel.clearCurrentPopupNode();
}
});
Expand Down
137 changes: 137 additions & 0 deletions src/games/strategy/triplea/ui/export/ScreenshotExporter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package games.strategy.triplea.ui.export;

import static com.google.common.base.Preconditions.checkNotNull;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Optional;

import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import games.strategy.engine.data.GameData;
import games.strategy.engine.history.HistoryNode;
import games.strategy.engine.history.Round;
import games.strategy.triplea.ui.IUIContext;
import games.strategy.triplea.ui.MapPanel;
import games.strategy.triplea.ui.TripleAFrame;
import games.strategy.triplea.ui.mapdata.MapData;
import games.strategy.ui.SwingComponents;
import games.strategy.ui.Util;

public final class ScreenshotExporter {
private final TripleAFrame frame;

private ScreenshotExporter(final TripleAFrame frame) {
this.frame = frame;
}

/**
* Prompts the user for the file to which the screenshot will be saved and saves the screenshot for the specified game
* at the specified history step to that file.
*
* @param frame The frame associated with the game screenshot to export; must not be {@code null}.
* @param gameData The game data; must not be {@code null}.
* @param node The history step at which the game screenshot is to be captured; must not be {@code null}.
*/
public static void exportScreenshot(final TripleAFrame frame, final GameData gameData, final HistoryNode node) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I discussed in a previous comment, it wasn't clear to me what is the best, if any, way to remove the reference to TripleAFrame. Please advise how you'd like to proceed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's reasonable enough to have a TripleAFrame object for the moment. A next iteration probably could get rid of it. Instead would still need references for:

    final IUIContext iuiContext = frame.getUIContext();
    final MapPanel mapPanel = frame.getMapPanel();

The rest is Swing code that is mainly used to know where to center the next icon/dialog. It's possible to get rid of those, and to have pop-up windows simply be centered. In one sense we should try to prefer those kinds of APIs since there is no extra JFrame component being passed around and wired all over the place.

checkNotNull(frame);
checkNotNull(gameData);
checkNotNull(node);

final ScreenshotExporter exporter = new ScreenshotExporter(frame);
exporter.promptSaveFile().ifPresent(file -> exporter.runSave(gameData, node, file));
}

private Optional<File> promptSaveFile() {
return SwingComponents.promptSaveFile(frame, "png", "Saved Map Snapshots");
}

private void runSave(final GameData gameData, final HistoryNode node, final File file) {
SwingComponents.runWithProgressBar(frame, "Saving map snapshot...", () -> {
save(gameData, node, file);
return null;
}).whenComplete((ignore, e) -> {
SwingUtilities.invokeLater(() -> {
if (e == null) {
JOptionPane.showMessageDialog(frame, "Map Snapshot Saved", "Map Snapshot Saved",
JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(frame, e.getMessage(), "Error Saving Map Snapshot", JOptionPane.ERROR_MESSAGE);
}
});
});
}

private void save(final GameData gameData, final HistoryNode node, final File file) throws IOException {
// get round/step/player from history tree
int round = 0;
final Object[] pathFromRoot = node.getPath();
for (final Object pathNode : pathFromRoot) {
final HistoryNode curNode = (HistoryNode) pathNode;
if (curNode instanceof Round) {
round = ((Round) curNode).getRoundNo();
}
}
final IUIContext iuiContext = frame.getUIContext();
final double scale = iuiContext.getScale();
// print map panel to image
final MapPanel mapPanel = frame.getMapPanel();
final BufferedImage mapImage =
Util.createImage((int) (scale * mapPanel.getImageWidth()), (int) (scale * mapPanel.getImageHeight()), false);
final Graphics2D mapGraphics = mapImage.createGraphics();
try {
// workaround to get the whole map
// (otherwise the map is cut if current window is not on top of map)
final int xOffset = mapPanel.getXOffset();
final int yOffset = mapPanel.getYOffset();
mapPanel.setTopLeft(0, 0);
mapPanel.drawMapImage(mapGraphics);
mapPanel.setTopLeft(xOffset, yOffset);
// overlay title
Color title_color = iuiContext.getMapData().getColorProperty(MapData.PROPERTY_SCREENSHOT_TITLE_COLOR);
if (title_color == null) {
title_color = Color.BLACK;
}
final String s_title_x = iuiContext.getMapData().getProperty(MapData.PROPERTY_SCREENSHOT_TITLE_X);
final String s_title_y = iuiContext.getMapData().getProperty(MapData.PROPERTY_SCREENSHOT_TITLE_Y);
final String s_title_size = iuiContext.getMapData().getProperty(MapData.PROPERTY_SCREENSHOT_TITLE_FONT_SIZE);
int title_x;
int title_y;
int title_size;
try {
title_x = (int) (Integer.parseInt(s_title_x) * scale);
title_y = (int) (Integer.parseInt(s_title_y) * scale);
title_size = Integer.parseInt(s_title_size);
} catch (final NumberFormatException nfe) {
// choose safe defaults
title_x = (int) (15 * scale);
title_y = (int) (15 * scale);
title_size = 15;
}
// everything else should be scaled down onto map image
final AffineTransform transform = new AffineTransform();
transform.scale(scale, scale);
mapGraphics.setTransform(transform);
mapGraphics.setFont(new Font("Ariel", Font.BOLD, title_size));
mapGraphics.setColor(title_color);
if (iuiContext.getMapData().getBooleanProperty(MapData.PROPERTY_SCREENSHOT_TITLE_ENABLED)) {
mapGraphics.drawString(gameData.getGameName() + " Round " + round, title_x, title_y);
}

// save Image as .png
ImageIO.write(mapImage, "png", file);
} finally {
// Clean up objects. There might be some overkill here,
// but there were memory leaks that are fixed by some/all of these.
mapImage.flush();
mapGraphics.dispose();
}
}
}
137 changes: 3 additions & 134 deletions src/games/strategy/triplea/ui/menubar/ExportMenu.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package games.strategy.triplea.ui.menubar;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
Expand All @@ -20,7 +15,6 @@
import java.util.List;
import java.util.Set;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
Expand All @@ -30,7 +24,6 @@
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.WindowConstants;
import javax.swing.filechooser.FileFilter;
import javax.swing.tree.DefaultMutableTreeNode;

import games.strategy.debug.ClientLogger;
Expand All @@ -41,7 +34,6 @@
import games.strategy.engine.data.UnitType;
import games.strategy.engine.data.export.GameDataExporter;
import games.strategy.engine.framework.GameDataUtils;
import games.strategy.engine.framework.ui.SaveGameFileChooser;
import games.strategy.engine.history.HistoryNode;
import games.strategy.engine.history.Round;
import games.strategy.engine.history.Step;
Expand All @@ -51,13 +43,11 @@
import games.strategy.triplea.printgenerator.SetupFrame;
import games.strategy.triplea.ui.ExtendedStats;
import games.strategy.triplea.ui.IUIContext;
import games.strategy.triplea.ui.MapPanel;
import games.strategy.triplea.ui.TripleAFrame;
import games.strategy.triplea.ui.export.ScreenshotExporter;
import games.strategy.triplea.ui.history.HistoryPanel;
import games.strategy.triplea.ui.mapdata.MapData;
import games.strategy.triplea.util.PlayerOrderComparator;
import games.strategy.ui.SwingAction;
import games.strategy.ui.Util;
import games.strategy.util.IllegalCharacterRemover;
import games.strategy.util.LocalizeHTML;

Expand Down Expand Up @@ -128,140 +118,19 @@ private void exportXMLFile() {

private void addSaveScreenshot(final JMenu parentMenu) {
final AbstractAction abstractAction = SwingAction.of("Export Map Snapshot", e -> {

// get current history node. if we are in history view, get the selected node.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was originally in saveScreenshot() and appeared to have been orphaned there when the following code was moved to this location at some point in the past. I moved it simply to avoid confusion in saveScreenshot().

final HistoryPanel historyPanel = frame.getHistoryPanel();
final HistoryNode curNode;
if (historyPanel == null) {
curNode = gameData.getHistory().getLastNode();
} else {
curNode = historyPanel.getCurrentNode();
}
saveScreenshot(curNode, frame, gameData);
ScreenshotExporter.exportScreenshot(frame, gameData, curNode);
});
parentMenu.add(abstractAction).setMnemonic(KeyEvent.VK_E);
}

public static void saveScreenshot(final HistoryNode node, final TripleAFrame frame, final GameData gameData) {
final FileFilter pngFilter = new FileFilter() {
@Override
public boolean accept(final File f) {
if (f.isDirectory()) {
return true;
} else {
return f.getName().endsWith(".png");
}
}

@Override
public String getDescription() {
return "Saved Screenshots, *.png";
}
};
final JFileChooser fileChooser = new SaveGameFileChooser();
fileChooser.setFileFilter(pngFilter);
final int rVal = fileChooser.showSaveDialog(null);
if (rVal == JFileChooser.APPROVE_OPTION) {
File f = fileChooser.getSelectedFile();
if (!f.getName().toLowerCase().endsWith(".png")) {
f = new File(f.getParent(), f.getName() + ".png");
}
// A small warning so users will not over-write a file,
if (f.exists()) {
final int choice =
JOptionPane.showConfirmDialog(null, "A file by that name already exists. Do you wish to over write it?",
"Over-write?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (choice != JOptionPane.OK_OPTION) {
return;
}
}
final File file = f;
final Runnable t = () -> {
if (saveScreenshot(node, file, frame, gameData)) {
JOptionPane.showMessageDialog(null, "Map Snapshot Saved", "Map Snapshot Saved",
JOptionPane.INFORMATION_MESSAGE);
}
};
SwingAction.invokeAndWait(t);
}
}

private static boolean saveScreenshot(final HistoryNode node, final File file, final TripleAFrame frame,
final GameData gameData) {
// get current history node. if we are in history view, get the selected node.
boolean retval = true;
// get round/step/player from history tree
int round = 0;
final Object[] pathFromRoot = node.getPath();
for (final Object pathNode : pathFromRoot) {
final HistoryNode curNode = (HistoryNode) pathNode;
if (curNode instanceof Round) {
round = ((Round) curNode).getRoundNo();
}
}
final IUIContext iuiContext = frame.getUIContext();
final double scale = iuiContext.getScale();
// print map panel to image

final MapPanel mapPanel = frame.getMapPanel();
final BufferedImage mapImage =
Util.createImage((int) (scale * mapPanel.getImageWidth()), (int) (scale * mapPanel.getImageHeight()), false);
final Graphics2D mapGraphics = mapImage.createGraphics();
try {
// workaround to get the whole map
// (otherwise the map is cut if current window is not on top of map)
final int xOffset = mapPanel.getXOffset();
final int yOffset = mapPanel.getYOffset();
mapPanel.setTopLeft(0, 0);
mapPanel.print(mapGraphics);
mapPanel.setTopLeft(xOffset, yOffset);
// overlay title
Color title_color = iuiContext.getMapData().getColorProperty(MapData.PROPERTY_SCREENSHOT_TITLE_COLOR);
if (title_color == null) {
title_color = Color.BLACK;
}
final String s_title_x = iuiContext.getMapData().getProperty(MapData.PROPERTY_SCREENSHOT_TITLE_X);
final String s_title_y = iuiContext.getMapData().getProperty(MapData.PROPERTY_SCREENSHOT_TITLE_Y);
final String s_title_size = iuiContext.getMapData().getProperty(MapData.PROPERTY_SCREENSHOT_TITLE_FONT_SIZE);
int title_x;
int title_y;
int title_size;
try {
title_x = (int) (Integer.parseInt(s_title_x) * scale);
title_y = (int) (Integer.parseInt(s_title_y) * scale);
title_size = Integer.parseInt(s_title_size);
} catch (final NumberFormatException nfe) {
// choose safe defaults
title_x = (int) (15 * scale);
title_y = (int) (15 * scale);
title_size = 15;
}
// everything else should be scaled down onto map image
final AffineTransform transform = new AffineTransform();
transform.scale(scale, scale);
mapGraphics.setTransform(transform);
mapGraphics.setFont(new Font("Ariel", Font.BOLD, title_size));
mapGraphics.setColor(title_color);
if (iuiContext.getMapData().getBooleanProperty(MapData.PROPERTY_SCREENSHOT_TITLE_ENABLED)) {
mapGraphics.drawString(gameData.getGameName() + " Round " + round, title_x, title_y);
}

// save Image as .png
try {
ImageIO.write(mapImage, "png", file);
} catch (final Exception e2) {
e2.printStackTrace();
JOptionPane.showMessageDialog(frame, e2.getMessage(), "Error saving Screenshot", JOptionPane.OK_OPTION);
retval = false;
}
// Clean up objects. There might be some overkill here,
// but there were memory leaks that are fixed by some/all of these.
} finally {
mapImage.flush();
mapGraphics.dispose();
}
return retval;
}

private void addExportStatsFull(final JMenu parentMenu) {
final Action showDiceStats = SwingAction.of("Export Full Game Stats", e -> createAndSaveStats(true));
parentMenu.add(showDiceStats).setMnemonic(KeyEvent.VK_F);
Expand Down
Loading