/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.client.ui;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.event.IIOWriteProgressListener;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JToggleButton;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import com.jeta.forms.components.panel.FormPanel;
import com.jeta.forms.gui.form.FormAccessor;
import com.t3.client.TabletopTool;
import com.t3.client.ui.zone.PlayerView;
import com.t3.client.ui.zone.ZoneRenderer;
import com.t3.language.I18N;
import com.t3.model.Player;
import com.t3.model.Zone;
import com.t3.model.drawing.DrawablePaint;
import com.t3.model.drawing.DrawableTexturePaint;
import com.t3.net.FTPLocation;
import com.t3.net.LocalLocation;
import com.t3.net.Location;
import com.t3.swing.SwingUtil;
import com.t3.util.ImageManager;
/**
* Creates a dialog for performing a screen capture to a PNG file.
* <p>
* This uses a modal dialog based on an Abeille form. It creates a PNG file at
* the resolution of the 'board' image/tile. The file can be saved to disk or
* sent to an FTP location.
*
* @return a dialog box
*/
@SuppressWarnings("serial")
public class ExportDialog extends JDialog implements IIOWriteProgressListener {
//
// Dialog/ UI related vars
//
private static final Logger log = Logger.getLogger(ExportDialog.class);
/** the modal panel the user uses to select the screenshot options */
private static FormPanel interactPanel;
/** The modal panel showing screenshot progress */
private static JLabel progressLabel;
/** The place the image will be sent to (file/FTP) */
private Location exportLocation;
// These are convenience variables, which should be set
// each time the dialog is shown. It is safe to
// cache them like this since the dialog is modal.
// If this dialog is ever not modal, these need to be
// factored out.
private static Zone zone;
private static ZoneRenderer renderer;
// These are used to preserve zone settings because
// we'll change the Zone/ZoneRenderer temporarily to take the screenshot.
// These are static because we don't expect more than
// a single ExportDialog to ever be instanced.
// Pseudo-layers
private static Zone.VisionType savedVision;
private static boolean savedFog;
private static boolean savedBoard;
// real layers
private static boolean savedToken;
private static boolean savedHidden;
private static boolean savedObject;
private static boolean savedBackground;
// for ZoneRenderer preservation
private static Rectangle origBounds;
private static Scale origScale;
/** set by preScreenshot, cleared by postScreenshot */
private boolean waitingForPostScreenshot = false;
/**
* Only doing this because I don't expect more than one instance of this
* modal dialog
*/
private static int instanceCount = 0;
//
// Vars for background rendering of the screenshot
//
/** 0-100: percentage of pixels written to destination */
private int renderPercent;
//
// TODO: BUG: transparent objects get less transparent with each render?
// TODO: BUG: stamps disappearing during and after rendering, come back with movement.
//
//
// TODO: Abeille should auto-generate most of this code:
// 1. We shouldn't have to synchronize the names of variables manually
// 2. Specifying the name of a button in Abeille is the same as declaring a variable
// 3. This code is always the same for every form, aside from the var names
// 4. JAVA doesn't have a way to do abstract enumerated types, so we can't re-use the code except by copy/paste
// 5. Abeille seems to be abandonded at this point (July 2010). The owner replied as recently as July 2009, but
// seems not to have followed up.
//
/**
* This enum is for ALL the radio buttons in the dialog, regardless of their
* grouping.
* <p>
* The names of the enums should be the same as the button names.
*/
public static enum ExportRadioButtons {
// Format of enum declaration:
// [Abeille Forms Designer button name] (default checked, default enabled)
// Button Group 1 (not that it matters for this controller)
TYPE_CURRENT_VIEW,
TYPE_ENTIRE_MAP,
// Button Group 2
VIEW_GM,
VIEW_PLAYER,
// Button Group 3
LAYERS_CURRENT,
LAYERS_AS_SELECTED;
private static FormPanel form;
//
// SetForm stores the form this is attached to
//
public static void setForm(FormPanel form) {
ExportRadioButtons.form = form;
for (ExportRadioButtons button : ExportRadioButtons.values()) {
try {
if (form.getRadioButton(button.toString()) == null) {
throw new Exception("Export Dialog has a mis-matched enum: " + button.toString());
}
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
enforceButtonRules();
}
});
} catch (Exception ex) {
TabletopTool.showError("dialog.screenshot.radio.button.uiImplementationError", ex);
}
}
}
//
// Generic utility methods
// NON-Static
//
public void setChecked(boolean checked) {
form.getRadioButton(this.toString()).setSelected(checked);
}
public boolean isChecked() {
return form.getRadioButton(this.toString()).isSelected();
}
public void setEnabled(boolean enabled) {
form.getRadioButton(this.toString()).setEnabled(enabled);
}
/**
* Shortcut to allow clean code and type-checking of invocations of
* specific buttons
*/
public void addActionListener(ActionListener listener) {
form.getRadioButton(this.toString()).addActionListener(listener);
}
/**
* @return which of the buttons in the Type group is selected
*/
public static ExportRadioButtons getType() {
if (ExportRadioButtons.TYPE_CURRENT_VIEW.isChecked()) {
return TYPE_CURRENT_VIEW;
} else if (ExportRadioButtons.TYPE_ENTIRE_MAP.isChecked()) {
return TYPE_ENTIRE_MAP;
}
return null;
}
/**
* @return which of the buttons in the View group is selected
*/
public static ExportRadioButtons getView() {
if (ExportRadioButtons.VIEW_GM.isChecked()) {
return VIEW_GM;
} else if (ExportRadioButtons.VIEW_PLAYER.isChecked()) {
return VIEW_PLAYER;
}
return null;
}
/**
* @return which of the buttons in the Layers group is selected
*/
public static ExportRadioButtons getLayers() {
if (ExportRadioButtons.LAYERS_CURRENT.isChecked()) {
return LAYERS_CURRENT;
} else if (ExportRadioButtons.LAYERS_AS_SELECTED.isChecked()) {
return LAYERS_AS_SELECTED;
}
return null;
}
}
/**
* This enum is for all the checkboxes which select layers.
* <p>
* The names of the enums should be the same as the button names.
*/
private static enum ExportLayers {
// enum_val (fieldName as per Abeille Forms Designer, playerCanModify)
LAYER_TOKEN(true),
LAYER_HIDDEN(false),
LAYER_OBJECT(false),
LAYER_BACKGROUND(false),
LAYER_BOARD(false),
LAYER_FOG(false),
LAYER_VISIBILITY(true);
private static FormPanel form;
private final boolean playerCanModify;
/**
* Constructor, sets rules for export of this layer. 'Player' is in
* reference to the Role type (Player vs. GM).
*/
ExportLayers(boolean playerCanModify) {
this.playerCanModify = playerCanModify;
}
/**
* Stores the form this is attached to, so we don't have to store
* duplicate data locally (like selected and enabled). Also perform some
* error checking, since we _are_ duplicating the description of the
* form itself (like what buttons it has).
*
* @param form
* The FormPanel this dialog is part of.
*/
public static void setForm(FormPanel form) {
ExportLayers.form = form;
for (ExportLayers button : ExportLayers.values()) {
try {
if (form.getButton(button.toString()) == null) {
throw new Exception("Export Dialog has a mis-matched enum: " + button.toString());
}
} catch (Exception ex) {
TabletopTool.showError(I18N.getString("dialog.screenshot.layer.button.uiImplementationError"), ex);
}
}
}
//
// Misc utility methods
//
public void setChecked(boolean checked) {
form.getButton(this.toString()).setSelected(checked);
}
public boolean isChecked() {
return form.getButton(this.toString()).isSelected();
}
public void setEnabled(boolean enabled) {
form.getButton(this.toString()).setEnabled(enabled);
}
/**
* Sets the layer-selection checkboxes to replicate the "current view".
*/
public void setToDefault() {
final Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone();
if (this == ExportLayers.LAYER_FOG) {
ExportLayers.LAYER_FOG.setChecked(zone.hasFog());
} else if (this == ExportLayers.LAYER_VISIBILITY) {
ExportLayers.LAYER_VISIBILITY.setChecked(zone.getVisionType() != Zone.VisionType.OFF);
} else {
setChecked(true);
}
}
public static void setDefaultChecked() {
// everything defaults to 'on' since the layers don't really have on/off capability
// outside of this screenshot code
for (ExportLayers layer : ExportLayers.values()) {
layer.setChecked(true);
}
// however, some pseudo-layers do have a state, so set that appropriately
final Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone();
ExportLayers.LAYER_VISIBILITY.setChecked(zone.getVisionType() != Zone.VisionType.OFF);
ExportLayers.LAYER_FOG.setChecked(zone.hasFog());
}
public static void setDisabled() {
for (ExportLayers layer : ExportLayers.values()) {
layer.setEnabled(false);
}
}
}
/**
* Ensures that the user can only check/uncheck boxes as appropriate. For
* example, if "fog" is not enabled on the map, it cannot be enabled for
* export.
* <p>
* This should get called during initialization and whenever the radio
* buttons change.
* <p>
* The GM and Players have different rules, to prevent players from gaining
* knowledge they should not have using the screenshot (such as revealing
* things under other things by disabling layers). Players can basically
* only turn off tokens, to get an 'empty' version of the map.
*/
public static void enforceButtonRules() {
if (!TabletopTool.getPlayer().isGM()) {
ExportRadioButtons.VIEW_PLAYER.setChecked(true);
ExportRadioButtons.VIEW_PLAYER.setEnabled(true);
ExportRadioButtons.VIEW_GM.setEnabled(false);
}
if (ExportRadioButtons.LAYERS_CURRENT.isChecked()) {
// By "current layers" we mean what you see in the editor, which is everything.
// So disable mucking about with layers.
interactPanel.getLabel("LAYERS_LABEL").setEnabled(false);
ExportLayers.setDefaultChecked();
ExportLayers.setDisabled();
} else /* if (ExportRadioButtons.LAYERS_AS_SELECTED.isChecked()) */{
interactPanel.getLabel("LAYERS_LABEL").setEnabled(true);
boolean isGM = ExportRadioButtons.VIEW_GM.isChecked();
final Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone();
for (ExportLayers layer : ExportLayers.values()) {
boolean enabled = isGM || layer.playerCanModify;
// Regardless of whether it is a player or GM,
// only enable fog and visibility check-boxes
// when the map has those things turned on.
switch (layer) {
case LAYER_VISIBILITY:
enabled &= (zone.getVisionType() != Zone.VisionType.OFF);
break;
case LAYER_FOG:
enabled &= zone.hasFog();
break;
}
layer.setEnabled(enabled);
if (!enabled) {
layer.setToDefault();
}
}
}
}
public ExportDialog() throws Exception {
super(TabletopTool.getFrame(), "Export Screenshot", true);
if (instanceCount == 0) {
instanceCount++;
} else {
throw new Exception("Only one instance of ExportDialog allowed!");
}
// The window uses about 1MB. Disposing frees this, but repeated uses
// will cause more memory fragmentation.
// MCL: I figure it's better to save the 1MB for low-mem systems,
// but it would be even better to HIDE it, and then dispose() it
// when the user clicks on the memory meter to free memory
// setDefaultCloseOperation(HIDE_ON_CLOSE);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
//
// Initialize the panel and button actions
//
createWaitPanel();
interactPanel = new FormPanel("com/t3/client/ui/forms/exportDialog.xml");
setLayout(new GridLayout());
add(interactPanel);
getRootPane().setDefaultButton((JButton) interactPanel.getButton("exportButton"));
pack();
ExportRadioButtons.setForm(interactPanel);
ExportLayers.setForm(interactPanel);
interactPanel.getButton("exportButton").addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
exportButtonAction();
}
});
interactPanel.getButton("cancelButton").addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
dispose();
}
});
interactPanel.getButton("browseButton").addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
browseButtonAction();
}
});
// Run this once to make sure the dialog is in a good starting state.
ExportLayers.setDefaultChecked();
enforceButtonRules();
}
@Override
public void setVisible(boolean b) {
if (b) {
// Always call this first, since other methods may rely on zone or renderer being set.
setZone(TabletopTool.getFrame().getCurrentZoneRenderer().getZone());
setZoneRenderer(TabletopTool.getFrame().getCurrentZoneRenderer());
// Set to interactive mode
switchToInteractPanel();
// In case something changed while the dialog was closed...
enforceButtonRules();
SwingUtil.centerOver(this, TabletopTool.getFrame());
} else {
if (waitingForPostScreenshot) {
postScreenshot();
}
}
super.setVisible(b);
}
//
// These get/set the convenience variables zone and renderer
//
public static void setZone(Zone zone) {
ExportDialog.zone = zone;
}
public static ZoneRenderer getZoneRenderer() {
return renderer;
}
public static void setZoneRenderer(ZoneRenderer renderer) {
ExportDialog.renderer = renderer;
}
public static Zone getZone() {
return zone;
}
private void exportButtonAction() {
// This block is to allow preservation of existing dialog behavior:
// Neither button is set when the dialog first appears, so we have to
// make sure the user picks one. Presumably this is to force the user
// to pay attention to this choice and not just accept a default.
if (!(ExportRadioButtons.VIEW_GM.isChecked() || ExportRadioButtons.VIEW_PLAYER.isChecked())) {
TabletopTool.showError(I18N.getString("dialog.screenshot.error.mustSelectView"), null);
return;
}
// LOCATION
// TODO: Show a progress dialog
// TODO: Make this less fragile
switch (interactPanel.getTabbedPane("tabs").getSelectedIndex()) {
case 0:
File file = new File(interactPanel.getText("locationTextField").trim());
// PNG only supported for now
if (file.getName().endsWith("/")) {
TabletopTool.showError("Filename must not end with a slash ('/')");
return;
} else if (!file.getName().toLowerCase().endsWith(".png")) {
file = new File(file.getAbsolutePath() + ".png");
}
exportLocation = new LocalLocation(file);
break;
case 1:
String username = interactPanel.getText("username").trim();
String password = interactPanel.getText("password").trim();
String host = interactPanel.getText("host").trim();
String path = interactPanel.getText("path").trim();
// PNG only supported for now
if (path.endsWith("/")) {
TabletopTool.showError("Path must not end with a slash ('/')");
return;
} else if (!path.toLowerCase().endsWith(".png")) {
path += ".png";
}
exportLocation = new FTPLocation(username, password, host, path);
break;
}
try {
screenCapture();
} catch (Exception ex) {
TabletopTool.showError(I18N.getString("dialog.screenshot.error.failedExportingImage"), ex);
} finally {
dispose();
}
}
public void browseButtonAction() {
JFileChooser chooser = new JFileChooser();
if (exportLocation instanceof LocalLocation) {
chooser.setSelectedFile(((LocalLocation) exportLocation).getFile());
}
if (chooser.showOpenDialog(ExportDialog.this) == JFileChooser.APPROVE_OPTION) {
interactPanel.setText("locationTextField", chooser.getSelectedFile().getAbsolutePath());
}
}
/**
* This is the top-level screen-capture routine. It sends the resulting PNG
* image to the location previously selected by the user. TODO: It currently
* calls {@link TabletopTool#takeMapScreenShot()} for "normal" screenshots, but
* that's just until this code is considered stable enough.
*
* @throws Exception
*/
@SuppressWarnings("unused")
public void screenCapture() throws Exception {
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.GeneratingScreenshot"));
ExportRadioButtons type = ExportRadioButtons.getType();
Player.Role role;
try {
switch (type) {
case TYPE_CURRENT_VIEW:
// This uses the original screenshot code: I didn't want to touch it, so I need
// to pass it the same parameter it took before.
role = ExportRadioButtons.VIEW_GM.isChecked() ? Player.Role.GM : Player.Role.PLAYER;
BufferedImage screenCap = TabletopTool.takeMapScreenShot(renderer.getPlayerView(role));
// since old screenshot code doesn't throw exceptions, look for null
if (screenCap == null) {
throw new Exception(I18N.getString("dialog.screenshot.error.failedImageGeneration"));
}
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.screenshotStreaming"));
ByteArrayOutputStream imageOut = new ByteArrayOutputStream();
try {
ImageIO.write(screenCap, "png", imageOut);
screenCap = null; // Free up the memory as soon as possible
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.screenshotSaving"));
exportLocation.putContent(new BufferedInputStream(new ByteArrayInputStream(imageOut.toByteArray())));
} finally {
IOUtils.closeQuietly(imageOut);
}
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.screenshotSaved"));
break;
case TYPE_ENTIRE_MAP:
switchToWaitPanel();
if (interactPanel.isSelected("METHOD_BUFFERED_IMAGE") || interactPanel.isSelected("METHOD_IMAGE_WRITER")) {
// Using a buffer in memory for the whole image
try {
final PlayerView view = preScreenshot();
final ImageWriter pngWriter = ImageIO.getImageWritersByFormatName("png").next();
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.screenshotStreaming"));
BufferedImage image;
if (interactPanel.isSelected("METHOD_BUFFERED_IMAGE")) {
image = new BufferedImage(renderer.getWidth(), renderer.getHeight(), Transparency.OPAQUE);
final Graphics2D g = image.createGraphics();
// g.setClip(0, 0, renderer.getWidth(), renderer.getHeight());
renderer.renderZone(g, view);
g.dispose();
} else {
image = new ZoneImageGenerator(renderer, view);
}
// putContent() can consume quite a bit of time; really should have a progress
// meter of some kind here.
exportLocation.putContent(pngWriter, image);
if (image instanceof ZoneImageGenerator) {
log.debug("ZoneImageGenerator() stats: " + image.toString());
}
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.screenshotSaving"));
} catch (Exception e) {
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.error.failedImageGeneration"));
} finally {
postScreenshot();
TabletopTool.getFrame().setStatusMessage(I18N.getString("dialog.screenshot.msg.screenshotSaved"));
}
} else if (interactPanel.isSelected("METHOD_BACKGROUND")) {
// We must call preScreenshot before creating the ZoneImageGenerator, because
// ZoneImageGenerator uses the ZoneRenderer's bounds to set itself up
TabletopTool.showError("This doesn't work! Try one of the other methods.", null);
if (false) {
//
// Note: this implementation is the obvious way, which doesn't work, since
// ZoneRenderer is part of the Swing component chain, and the threads get deadlocked.
//
// The suggested implementation by
// "Reiger" at http://ubuntuforums.org/archive/index.php/t-1455270.html
// might work... except that it would have to be part of ZoneRenderer
//
// The only way to make this really work is to pull the renderZone function
// out of ZoneRenderer into a new class: call it ZoneRasterizer. Then make
// ZoneRenderer create an instance of it, and patch up the code to make it
// compatible. Then we can create an instance of ZoneRasterizer, and run it
// in a separate thread, since it won't lock in any of the functions that
// Swing uses.
//
class backscreenRender implements Runnable {
@Override
public void run() {
try {
PlayerView view = preScreenshot();
final ZoneImageGenerator zoneImageGenerator = new ZoneImageGenerator(renderer, view);
final ImageWriter pngWriter = ImageIO.getImageWritersByFormatName("png").next();
exportLocation.putContent(pngWriter, zoneImageGenerator);
// postScreenshot is called by the callback imageComplete()
} catch (Exception e) {
assert false : "Unhandled Exception in renderOffScreen: '" + e.getMessage() + "'";
}
}
}
backscreenRender p = new backscreenRender();
new Thread(p).start();
repaint();
}
} else {
throw new Exception("Unknown rendering method!");
}
break;
default:
throw new Exception(I18N.getString("dialog.screenshot.error.invalidDialogSettings"));
}
} catch (OutOfMemoryError e) {
TabletopTool.showError("screenCapture() caught: Out Of Memory", e);
} catch (Exception ex) {
TabletopTool.showError("screenCapture() caught: ", ex);
}
}
public Map<String, Boolean> getExportSettings() {
Map<String, Boolean> settings = new HashMap<String, Boolean>(16);
FormAccessor fa = interactPanel.getFormAccessor();
Iterator<?> iter = fa.beanIterator(true);
while (iter.hasNext()) {
Object obj = iter.next();
if (obj instanceof JToggleButton) {
JToggleButton jtb = (JToggleButton) obj;
settings.put(jtb.getName(), jtb.isSelected());
}
}
return settings;
}
/**
* Turn off all JToggleButtons on the form. We don't care if we turn off
* fields that are normally turned on, since {@link #enforceButtonRules()}
* will turn them back on as appropriate.
*/
private void resetExportSettings() {
FormAccessor fa = interactPanel.getFormAccessor();
Iterator<?> iter = fa.beanIterator(true);
while (iter.hasNext()) {
Object obj = iter.next();
if (obj instanceof JToggleButton) {
JToggleButton jtb = (JToggleButton) obj;
jtb.setSelected(false);
}
}
}
public void setExportSettings(Map<String, Boolean> settings) {
resetExportSettings();
if (settings != null) {
for (String iter : settings.keySet()) {
JToggleButton jtb = (JToggleButton) interactPanel.getComponentByName(iter);
if (jtb == null) {
log.warn("GUI component for export setting '" + iter + "' not found.");
} else
jtb.setSelected(settings.get(iter));
}
}
}
public Location getExportLocation() {
return exportLocation;
}
public void setExportLocation(Location loc) {
exportLocation = loc;
}
/**
* This is a preserves the layer settings on the Zone object. It should be
* followed by restoreZone()
*
* @return the image to be saved to a file
*/
private static void setupZoneLayers() throws Exception, OutOfMemoryError {
final Zone zone = TabletopTool.getFrame().getCurrentZoneRenderer().getZone();
//
// Preserve settings
//
// psuedo-layers
savedVision = zone.getVisionType();
savedFog = zone.hasFog();
savedBoard = zone.drawBoard();
// real layers
savedToken = Zone.Layer.TOKEN.isEnabled();
savedHidden = Zone.Layer.GM.isEnabled();
savedObject = Zone.Layer.OBJECT.isEnabled();
savedBackground = Zone.Layer.BACKGROUND.isEnabled();
//
// set according to dialog options
//
zone.setHasFog(ExportLayers.LAYER_FOG.isChecked());
if (!ExportLayers.LAYER_VISIBILITY.isChecked())
zone.setVisionType(Zone.VisionType.OFF);
zone.setDrawBoard(ExportLayers.LAYER_BOARD.isChecked());
Zone.Layer.TOKEN.setEnabled(ExportLayers.LAYER_TOKEN.isChecked());
Zone.Layer.GM.setEnabled(ExportLayers.LAYER_HIDDEN.isChecked());
Zone.Layer.OBJECT.setEnabled(ExportLayers.LAYER_OBJECT.isChecked());
Zone.Layer.BACKGROUND.setEnabled(ExportLayers.LAYER_BACKGROUND.isChecked());
}
/**
* This restores the layer settings on the Zone object. It should follow
* setupZoneLayers().
*
* @return the image to be saved to a file
*/
private static void restoreZoneLayers() {
zone.setHasFog(savedFog);
zone.setVisionType(savedVision);
zone.setDrawBoard(savedBoard);
Zone.Layer.TOKEN.setEnabled(savedToken);
Zone.Layer.GM.setEnabled(savedHidden);
Zone.Layer.OBJECT.setEnabled(savedObject);
Zone.Layer.BACKGROUND.setEnabled(savedBackground);
}
/**
* Finds the extents of the map, sets up zone to be captured. If the user is
* the GM, the extents include every object and everything that has any
* area, such as 'fog' and 'visibility' objects.
* <p>
* If a background tiling texture is used, the image is aligned to it, so
* that it can be used on re-import as a new base map image.
* <p>
* If the user is a player (or GM posing as a player), the extents only go
* as far as the revealed fog-of-war.
* <p>
* Must be followed by postScreenshot at some point, or the Zone will be
* messed up.
*
* @return the image to be saved
*/
private PlayerView preScreenshot() throws Exception, OutOfMemoryError {
assert (!waitingForPostScreenshot) : "preScreenshot() called twice in a row!";
setupZoneLayers();
boolean viewAsPlayer = ExportRadioButtons.VIEW_PLAYER.isChecked();
// First, figure out the 'extents' of the canvas
// This will be later modified by the fog (for players),
// and by the tiling texture (for re-importing)
//
PlayerView view = new PlayerView(viewAsPlayer ? Player.Role.PLAYER : Player.Role.GM);
Rectangle extents = renderer.zoneExtents(view);
try {
// Clip to what the players know about (if applicable).
// This keeps the player from exporting the map to learn which
// direction has more 'stuff' in it.
if (viewAsPlayer) {
Rectangle fogE = renderer.fogExtents();
// TabletopTool.showError(fogE.x + " " + fogE.y + " " + fogE.width + " " + fogE.height);
if ((fogE.width < 0) || (fogE.height < 0)) {
TabletopTool.showError(I18N.getString("dialog.screenshot.error.negativeFogExtents")); // Image is not clipped to show only fog-revealed areas!"));
} else {
extents = extents.intersection(fogE);
}
}
} catch (Exception ex) {
throw (new Exception(I18N.getString("dialog.screenshot.error.noArea"), ex));
}
if ((extents == null) || (extents.width == 0) || (extents.height == 0)) {
throw (new Exception(I18N.getString("dialog.screenshot.error.noArea")));
}
// If output includes the tiling 'board' texture, move the upper-left corner
// to an integer multiple of the background tile (so it matches up on import).
// We don't need to move the lower-right corner because it doesn't matter for
// aligning on importing.
boolean drawBoard = ExportLayers.LAYER_BOARD.isChecked();
if (drawBoard) {
DrawablePaint paint = renderer.getZone().getBackgroundPaint();
DrawableTexturePaint dummy = new DrawableTexturePaint();
Integer tileX = 0, tileY = 0;
if (paint.getClass() == dummy.getClass()) {
Image bgTexture = ImageManager.getImage(((DrawableTexturePaint) paint).getAsset().getId());
tileX = bgTexture.getWidth(null);
tileY = bgTexture.getHeight(null);
Integer x = ((int) Math.floor((float) extents.x / tileX)) * tileX;
Integer y = ((int) Math.floor((float) extents.y / tileY)) * tileY;
extents.width = extents.width + (extents.x - x);
extents.height = extents.height + (extents.y - y);
extents.x = x;
extents.y = y;
}
}
// Save the original state of the renderer to restore later.
// Create a place to put the image, and
// set up the renderer to encompass the whole extents of the map.
origBounds = renderer.getBounds();
origScale = renderer.getZoneScale();
// Setup the renderer to use the new extents
Scale s = new Scale();
s.setOffset(-extents.x, -extents.y);
renderer.setZoneScale(s);
renderer.setBounds(extents);
waitingForPostScreenshot = true;
return view;
}
private void postScreenshot() {
assert waitingForPostScreenshot : "postScrenshot called withot preScreenshot";
renderer.setBounds(origBounds);
renderer.setZoneScale(origScale);
restoreZoneLayers();
waitingForPostScreenshot = false;
}
//
// Panel related functions
//
private void switchToWaitPanel() {
// remove(interactPanel);
// add(waitPanel);
// getRootPane().setDefaultButton(null);
// pack();
}
private void switchToInteractPanel() {
// remove(waitPanel);
// add(interactPanel);
// getRootPane().setDefaultButton((JButton) interactPanel.getButton("exportButton"));
// pack();
}
private void createWaitPanel() {
progressLabel = new JLabel();
imageProgress(null, 0);
}
//
// IIOWriteProgressListener Interface
//
/**
* Setup the progress meter.
*/
@Override
public void imageStarted(ImageWriter source, int imageIndex) {
renderPercent = 0;
progressLabel.setText(I18N.getText("exportDialog.msg.renderingWait" + renderPercent + "%"));
repaint();
}
/**
* Update the progress meter.
*/
@Override
public void imageProgress(ImageWriter source, float percentageDone) {
int oldPercent = renderPercent;
renderPercent = (int) (percentageDone * 100);
if (renderPercent > oldPercent) {
progressLabel.setText(I18N.getText("exportDialog.msg.renderingWait" + renderPercent + "%"));
repaint();
}
}
/**
* Close this dialog box upon completion of background thread renderer.
*/
@Override
public void imageComplete(ImageWriter source) {
postScreenshot();
dispose();
}
@Override
public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) {
}
@Override
public void thumbnailProgress(ImageWriter source, float percentageDone) {
}
@Override
public void thumbnailComplete(ImageWriter source) {
}
@Override
public void writeAborted(ImageWriter source) {
}
}