/* * 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) { } }