/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.jogl.awt; import static com.jogamp.opengl.GL.GL_BGRA; import static com.jogamp.opengl.GL.GL_DRAW_FRAMEBUFFER; import static com.jogamp.opengl.GL.GL_NEAREST; import static com.jogamp.opengl.GL.GL_READ_FRAMEBUFFER; import static com.jogamp.opengl.GL.GL_UNSIGNED_BYTE; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Arrays; import java.util.Hashtable; import java.util.regex.Pattern; import javax.imageio.ImageIO; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.WindowConstants; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.text.PlainDocument; import org.andork.awt.GridBagWizard; import org.andork.awt.I18n; import org.andork.awt.I18n.I18nUpdater; import org.andork.awt.I18n.Localizer; import org.andork.awt.IconScaler; import org.andork.awt.LocalizedException; import org.andork.awt.layout.RectangleUtils; import org.andork.bind.Binder; import org.andork.bind.BinderWrapper; import org.andork.bind.DefaultBinder; import org.andork.bind.QObjectAttributeBinder; import org.andork.bind.ui.ComponentTextBinder; import org.andork.bind.ui.ISelectorSelectionBinder; import org.andork.bind.ui.JSliderValueBinder; import org.andork.bind.ui.JSpinnerValueBinder; import org.andork.format.Format; import org.andork.jogl.DefaultJoglRenderer; import org.andork.jogl.GL3Framebuffer; import org.andork.jogl.JoglScene; import org.andork.jogl.JoglScreenPolygon; import org.andork.jogl.JoglViewSettings; import org.andork.jogl.awt.JoglExportImageDialogModel.ResolutionUnit; import org.andork.q.QObject; import org.andork.swing.BetterSpinnerNumberModel; import org.andork.swing.OnEDT; import org.andork.swing.async.SelfReportingTask; import org.andork.swing.async.SingleThreadedTaskService; import org.andork.swing.async.TaskService; import org.andork.swing.border.InnerGradientBorder; import org.andork.swing.event.EasyDocumentListener; import org.andork.swing.selector.DefaultSelector; import org.andork.swing.selector.ISelector; import org.andork.swing.selector.ISelectorListener; import org.andork.swing.text.Formats; import org.andork.swing.text.PatternDocumentFilter; import org.andork.swing.text.Patterns; import org.andork.swing.text.SimpleFormatter; import org.andork.swing.text.SimpleSpinnerEditor; import org.andork.util.StringUtils; import com.jogamp.nativewindow.awt.DirectDataBufferInt; import com.jogamp.nativewindow.awt.DirectDataBufferInt.BufferedImageInt; import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2ES2; import com.jogamp.opengl.GL3; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilities; import com.jogamp.opengl.GLProfile; import com.jogamp.opengl.awt.GLCanvas; @SuppressWarnings("serial") public class JoglExportImageDialog extends JDialog { private class CanvasHolderLayout implements LayoutManager { @Override public void addLayoutComponent(String name, Component comp) { } @Override public void layoutContainer(Container parent) { if (canvas == null) { return; } Rectangle insetBounds = RectangleUtils.insetCopy(new Rectangle(parent.getSize()), parent.getInsets()); Integer width = (Integer) pixelWidthSpinner.getValue(); Integer height = (Integer) pixelHeightSpinner.getValue(); if (width != null && height != null && width != 0 && height != 0) { Rectangle canvasBounds = new Rectangle(); if (width * insetBounds.height > insetBounds.width * height) { canvasBounds.width = insetBounds.width; canvasBounds.height = canvasBounds.width * height / width; } else { canvasBounds.height = insetBounds.height; canvasBounds.width = canvasBounds.height * width / height; } canvasBounds.x = insetBounds.x + insetBounds.width / 2 - canvasBounds.width / 2; canvasBounds.y = insetBounds.y + insetBounds.height / 2 - canvasBounds.height / 2; canvas.setBounds(canvasBounds); canvas.setVisible(true); } else { canvas.setVisible(false); } } @Override public Dimension minimumLayoutSize(Container parent) { if (canvas == null) { return new Dimension(0, 0); } return canvas.getMinimumSize(); } @Override public Dimension preferredLayoutSize(Container parent) { if (canvas == null) { return new Dimension(0, 0); } return canvas.getPreferredSize(); } @Override public void removeLayoutComponent(Component comp) { } } private class CaptureTask extends SelfReportingTask { public static final int MAX_TILE_WIDTH = 1024; public static final int MAX_TILE_HEIGHT = 1024; File outputFile; int[] tileWidths; int[] tileHeights; public CaptureTask() { super("Rendering image...", JoglExportImageDialog.this); } @Override protected void duringDialog() throws Exception { OnEDT.onEDT(() -> { dialog.setTitle("Exporting..."); try { outputFile = createOutputFile(); } catch (Exception ex) { return; } if (outputFile.exists()) { if (outputFile.isDirectory()) { JOptionPane.showMessageDialog(dialog, "Weird...output file " + outputFile + " is a directory", "Can't Export", JOptionPane.ERROR_MESSAGE); outputFile = null; return; } else { int option = JOptionPane.showConfirmDialog(dialog, "Output file " + outputFile + " already exists. Overwrite?", "Export Image", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); if (option != JOptionPane.OK_OPTION) { outputFile = null; return; } } } File outputDir = outputFile.getParentFile(); if (!outputDir.exists()) { int option = JOptionPane.showConfirmDialog(dialog, "Output directory " + outputDir + " does not exist. Create it?", "Export Image", JOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE); if (option == JOptionPane.OK_OPTION) { try { outputDir.mkdirs(); } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(dialog, "Failed to create directory " + outputDir + "; " + ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage(), "Export Failed", JOptionPane.ERROR_MESSAGE); outputFile = null; return; } } else { outputFile = null; return; } } Integer totalWidth = (Integer) pixelWidthSpinner.getValue(); Integer totalHeight = (Integer) pixelHeightSpinner.getValue(); if (totalWidth != null && totalHeight != null) { int xTiles = (totalWidth + MAX_TILE_WIDTH - 1) / MAX_TILE_WIDTH; tileWidths = new int[xTiles]; Arrays.fill(tileWidths, MAX_TILE_WIDTH); tileWidths[xTiles - 1] = totalWidth % MAX_TILE_WIDTH; int yTiles = (totalHeight + MAX_TILE_HEIGHT - 1) / MAX_TILE_HEIGHT; tileHeights = new int[yTiles]; Arrays.fill(tileHeights, MAX_TILE_HEIGHT); tileHeights[yTiles - 1] = totalHeight % MAX_TILE_HEIGHT; } }); if (outputFile == null || tileWidths == null || tileHeights == null || isCanceling()) { return; } setStatus("Rendering image..."); setIndeterminate(false); setCompleted(0); setTotal(tileWidths.length * tileHeights.length); BufferedImage capturedImage; try { renderer.startCapture(this); while (getCompleted() < getTotal()) { if (isCanceling()) { return; } canvas.invoke(true, gl -> { return true; }); setCompleted(getCompleted() + 1); } if (isCanceling()) { return; } } catch (OutOfMemoryError e) { e.printStackTrace(); OnEDT.onEDT(() -> { JOptionPane.showMessageDialog(dialog, e.getClass().getSimpleName() + ": " + e.getLocalizedMessage(), "Export Failed", JOptionPane.ERROR_MESSAGE); }); return; } finally { capturedImage = renderer.endCapture(); } setStatus("Saving image to " + outputFile + "..."); setIndeterminate(true); if (!isCanceling()) { try { ImageIO.write(capturedImage, "png", outputFile); OnEDT.onEDT(() -> { Integer value = (Integer) fileNumberSpinner.getValue(); if (value == null) { fileNumberSpinner.setValue(1); } else { fileNumberSpinner.setValue(value + 1); } dispose(); }); } catch (final Exception ex) { ex.printStackTrace(); OnEDT.onEDT(() -> { JOptionPane.showMessageDialog(dialog, ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage(), "Export Failed", JOptionPane.ERROR_MESSAGE); }); } } } @Override public boolean isCancelable() { return true; } } private class NEWTInitializer extends SelfReportingTask { public NEWTInitializer() { super("Initializing preview...", JoglExportImageDialog.this); } @Override protected void duringDialog() throws Exception { final GLProfile glp = GLProfile.get(GLProfile.GL3); final GLCapabilities caps = new GLCapabilities(glp); if (canvas == null) { canvas = new GLCanvas(caps); if (sharedAutoDrawable != null) { canvas.setSharedAutoDrawable(sharedAutoDrawable); } canvas.addGLEventListener(renderer); canvas.invoke(false, drawable -> { GL2ES2 gl = (GL2ES2) drawable.getGL(); int[] temp = new int[1]; ((GL3) gl).glGetIntegerv(GL.GL_MAX_SAMPLES, temp, 0); SwingUtilities.invokeLater(() -> numSamplesSlider.setMaximum(temp[0])); return false; }); } canvasHolder.add(canvas); canvasHolder.revalidate(); } } private class Renderer extends DefaultJoglRenderer { volatile CaptureTask captureTask; BufferedImage capturedImage; int bufferWidth; int bufferHeight; int totalWidth; int totalHeight; GL3Framebuffer blitFramebuffer = new GL3Framebuffer(); JoglScreenPolygon screenPolygon; public Renderer() { super(new GL3Framebuffer(), 1); screenPolygon = new JoglScreenPolygon(); screenPolygon.setColor(1f, 1f, 1f, 1f); } @Override public void display(GLAutoDrawable drawable) { GL2ES2 gl = (GL2ES2) drawable.getGL(); CaptureTask captureTask = this.captureTask; gl.glViewport(0, 0, drawable.getSurfaceWidth(), drawable.getSurfaceHeight()); super.display(drawable); if (captureTask == null) { return; } int tileX = captureTask.getCompleted() / captureTask.tileHeights.length; int tileY = captureTask.getCompleted() % captureTask.tileHeights.length; int tileWidth = captureTask.tileWidths[tileX]; int tileHeight = captureTask.tileHeights[tileY]; int viewportX = 0; int viewportY = 0; for (int x = 0; x < tileX; x++) { viewportX -= captureTask.tileWidths[x]; } for (int y = 0; y < tileY; y++) { viewportY -= captureTask.tileHeights[y]; } gl.glViewport(viewportX, viewportY, totalWidth, totalHeight); viewState.update(viewSettings, totalWidth, totalHeight); GL3 gl3 = (GL3) gl; int renderingFbo = framebuffer.renderingFbo(gl3, bufferWidth, bufferHeight, desiredNumSamples); int blitFbo = blitFramebuffer.renderingFbo(gl3, bufferWidth, bufferHeight, 1); gl3.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, renderingFbo); if (scene != null) { scene.draw(viewState, gl, m, n); } gl3.glBindFramebuffer(GL_READ_FRAMEBUFFER, renderingFbo); gl3.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, blitFbo); gl3.glBlitFramebuffer(0, 0, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight, GL.GL_COLOR_BUFFER_BIT, GL_NEAREST); gl3.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); gl3.glBindFramebuffer(GL_READ_FRAMEBUFFER, blitFbo); BufferedImageInt tile = DirectDataBufferInt.createBufferedImage(tileWidth, tileHeight, BufferedImage.TYPE_INT_ARGB, new Point(0, 0), new Hashtable<Object, Object>()); DirectDataBufferInt tileBuffer = (DirectDataBufferInt) tile.getRaster().getDataBuffer(); gl.glReadPixels(0, 0, tileWidth, tileHeight, GL_BGRA, GL_UNSIGNED_BYTE, tileBuffer.getData()); gl3.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); if (captureTask.isCanceling()) { return; } Graphics2D g2 = capturedImage.createGraphics(); // for( int x = 0 ; x < tileWidth ; x++ ) // { // for( int y = 0 ; y < tileHeight ; y++ ) // { // // TODO: glReadPixels is not getting alpha values. This hack is a // workaround, // // but I have to figure out why glReadPixels is not working // properly. // int rgb = tile.getRGB( x , y ); // if( ( rgb & 0xff000000 ) != 0 ) // { // System.out.println( "tile: " + x + ", " + y + ": " + rgb ); // } // rgb |= 0xff000000; // tile.setRGB( x , y , rgb ); // } // } AffineTransform prevXform = g2.getTransform(); g2.translate(-viewportX, totalHeight - 1 + viewportY); g2.scale(1.0, -1.0); g2.drawImage(tile, 0, 0, null); g2.setTransform(prevXform); g2.dispose(); } @Override protected void drawScene(GLAutoDrawable drawable) { super.drawScene(drawable); if (captureTask != null) { int tileX = captureTask.getCompleted() / captureTask.tileWidths.length; int tileY = captureTask.getCompleted() % captureTask.tileHeights.length; int tileWidth = captureTask.tileWidths[tileX]; int tileHeight = captureTask.tileHeights[tileY]; int previewWidth = drawable.getSurfaceWidth(); int previewHeight = drawable.getSurfaceHeight(); GL2ES2 gl = (GL2ES2) drawable.getGL(); int x1 = tileX * bufferWidth * previewWidth / totalWidth; int y1 = tileY * bufferHeight * previewHeight / totalHeight; int x2 = x1 + tileWidth * previewWidth / totalWidth; int y2 = y1 + tileHeight * previewHeight / totalHeight; screenPolygon.setPoints(2, x1, y1, x1, y2, x2, y2, x2, y1, x1, y1); screenPolygon.draw(viewState, gl, m, n); } } public BufferedImage endCapture() { captureTask = null; return capturedImage; } public void startCapture(CaptureTask captureTask) { this.captureTask = captureTask; bufferWidth = max(captureTask.tileWidths); bufferHeight = max(captureTask.tileHeights); totalWidth = total(captureTask.tileWidths); totalHeight = total(captureTask.tileHeights); capturedImage = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_ARGB); } } /** * */ private static final long serialVersionUID = -4636573587180684807L; private static final BigDecimal IN_TO_CM = new BigDecimal("2.54"); private static final BigDecimal CM_TO_IN = new BigDecimal( 1.0 / IN_TO_CM .doubleValue()); public static void main(String[] args) { new OnEDT() { @Override public void run() throws Throwable { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); JoglExportImageDialog dialog = new JoglExportImageDialog(null, new I18n()); Binder<QObject<JoglExportImageDialogModel>> binder = new DefaultBinder<QObject<JoglExportImageDialogModel>>(); QObject<JoglExportImageDialogModel> model = JoglExportImageDialogModel.instance.newObject(); model.set(JoglExportImageDialogModel.outputDirectory, "screenshots"); model.set(JoglExportImageDialogModel.fileNamePrefix, "breakout-screenshot"); model.set(JoglExportImageDialogModel.fileNumber, 1); model.set(JoglExportImageDialogModel.pixelWidth, 600); model.set(JoglExportImageDialogModel.pixelHeight, 400); model.set(JoglExportImageDialogModel.resolution, new BigDecimal(300)); model.set(JoglExportImageDialogModel.resolutionUnit, JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_IN); dialog.setBinder(binder); binder.set(model); dialog.setSize(800, 600); dialog.setLocationRelativeTo(null); dialog.setVisible(true); } }; } private static int max(int... values) { int max = Integer.MIN_VALUE; for (int i : values) { max = Math.max(max, i); } return max; } private static int total(int... values) { int total = 0; for (int i : values) { total += i; } return total; } Localizer localizer; GLAutoDrawable sharedAutoDrawable; JPanel canvasHolder; GLCanvas canvas; Renderer renderer = new Renderer(); JFileChooser outputDirectoryChooser; JLabel outputDirectoryLabel; JTextField outputDirectoryField; JButton chooseOutputDirectoryButton; JLabel fileNamePrefixLabel; JTextField fileNamePrefixField; JLabel fileNumberLabel; JSpinner fileNumberSpinner; JLabel outputFormatLabel; DefaultSelector<?> outputFormatSelector; Icon warningIcon = IconScaler .rescale( UIManager .getIcon("OptionPane.warningIcon"), 20, 20); JLabel outputFileOrWarningLabel; JPanel outputFileOrWarningLabelHolder; JLabel pixelSizeHeaderLabel; JLabel pixelWidthLabel; JSpinner pixelWidthSpinner; JLabel pixelWidthUnitLabel; JLabel pixelHeightLabel; JSpinner pixelHeightSpinner; JLabel pixelHeightUnitLabel; JLabel resolutionLabel; JSpinner resolutionSpinner; DefaultSelector<JoglExportImageDialogModel.ResolutionUnit> resolutionUnitSelector; JLabel printSizeHeaderLabel; JLabel printWidthLabel; JSpinner printWidthSpinner; JLabel printHeightLabel; JSpinner printHeightSpinner; JLabel printUnitLabel; DefaultSelector<JoglExportImageDialogModel.PrintSizeUnit> printUnitSelector; JLabel numSamplesLabel; JSlider numSamplesSlider; JButton exportButton; JButton cancelButton; boolean updating; BinderWrapper<QObject<JoglExportImageDialogModel>> binder = new BinderWrapper<QObject<JoglExportImageDialogModel>>(); QObjectAttributeBinder<String> outputDirectoryBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.outputDirectory, binder); QObjectAttributeBinder<String> fileNamePrefixBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.fileNamePrefix, binder); QObjectAttributeBinder<Integer> fileNumberBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.fileNumber, binder); QObjectAttributeBinder<Integer> pixelWidthBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.pixelWidth, binder); QObjectAttributeBinder<Integer> pixelHeightBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.pixelHeight, binder); QObjectAttributeBinder<BigDecimal> resolutionBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.resolution, binder); QObjectAttributeBinder<ResolutionUnit> resolutionUnitBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.resolutionUnit, binder); QObjectAttributeBinder<Integer> numSamplesBinder = QObjectAttributeBinder .bind( JoglExportImageDialogModel.numSamples, binder); JoglScene scene; TaskService taskService; public JoglExportImageDialog(GLAutoDrawable sharedAutoDrawable, Dialog owner, I18n i18n) { super(owner); this.sharedAutoDrawable = sharedAutoDrawable; init(i18n); } public JoglExportImageDialog(GLAutoDrawable sharedAutoDrawable, Frame owner, I18n i18n) { super(owner); this.sharedAutoDrawable = sharedAutoDrawable; init(i18n); } public JoglExportImageDialog(GLAutoDrawable sharedAutoDrawable, I18n i18n) { super(); this.sharedAutoDrawable = sharedAutoDrawable; init(i18n); } public JoglExportImageDialog(GLAutoDrawable sharedAutoDrawable, Window owner, I18n i18n) { super(owner); this.sharedAutoDrawable = sharedAutoDrawable; init(i18n); } private JSpinner createBigDecimalSpinner(int maxIntegerDigits, int fractionDigits, BigDecimal step) { BigDecimal minFraction = new BigDecimal(StringUtils.multiply("0", fractionDigits - 1) + "1"); BetterSpinnerNumberModel<BigDecimal> model = BetterSpinnerNumberModel.newInstance(minFraction, minFraction, new BigDecimal(10).pow(maxIntegerDigits).subtract(minFraction), step); JSpinner spinner = new JSpinner(model); SimpleSpinnerEditor editor = new SimpleSpinnerEditor(spinner); editor.getTextField().setColumns(maxIntegerDigits + fractionDigits + 1); Format<BigDecimal> format = Formats.createBigDecimalFormat(maxIntegerDigits, fractionDigits); SimpleFormatter formatter = new SimpleFormatter(format); formatter.install(editor.getTextField()); Pattern pattern = Patterns.createNumberPattern(maxIntegerDigits, fractionDigits, true); PatternDocumentFilter docFilter = new PatternDocumentFilter(pattern); ((PlainDocument) editor.getTextField().getDocument()).setDocumentFilter(docFilter); spinner.setEditor(editor); editor.getTextField().putClientProperty("value", spinner.getValue()); return spinner; } protected void createBindings() { ComponentTextBinder.bind(outputDirectoryField, outputDirectoryBinder); ComponentTextBinder.bind(fileNamePrefixField, fileNamePrefixBinder); JSpinnerValueBinder.bind(fileNumberSpinner, Integer.class, fileNumberBinder); JSpinnerValueBinder.bind(pixelWidthSpinner, Integer.class, pixelWidthBinder); JSpinnerValueBinder.bind(pixelHeightSpinner, Integer.class, pixelHeightBinder); JSpinnerValueBinder.bind(resolutionSpinner, BigDecimal.class, resolutionBinder); ISelectorSelectionBinder.bind(resolutionUnitSelector, resolutionUnitBinder); JSliderValueBinder.bind(numSamplesSlider, numSamplesBinder); } protected void createComponents() { canvasHolder = new JPanel(); canvasHolder.setBackground(Color.GRAY); outputDirectoryLabel = new JLabel(); localizer.setText(outputDirectoryLabel, "outputDirectoryLabel.text"); outputDirectoryField = new JTextField(); chooseOutputDirectoryButton = new JButton(UIManager.getIcon("FileView.directoryIcon")); chooseOutputDirectoryButton.setMargin(new Insets(2, 2, 2, 2)); localizer.setToolTipText(chooseOutputDirectoryButton, "chooseOutputDirectoryButton.toolTipText"); fileNamePrefixLabel = new JLabel(); localizer.setText(fileNamePrefixLabel, "fileNamePrefixLabel.text"); fileNamePrefixField = new JTextField(); fileNumberLabel = new JLabel(); localizer.setText(fileNumberLabel, "nextFileNumberLabel.text"); fileNumberSpinner = createIntegerSpinner(4, 1); outputFileOrWarningLabel = new JLabel(); outputFileOrWarningLabelHolder = new JPanel(); pixelSizeHeaderLabel = new JLabel(); localizer.setText(pixelSizeHeaderLabel, "pixelSizeHeaderLabel.text"); pixelWidthLabel = new JLabel(); localizer.setText(pixelWidthLabel, "widthLabel.text"); pixelWidthSpinner = createIntegerSpinner(6, 10); pixelWidthUnitLabel = new JLabel(); localizer.setText(pixelWidthUnitLabel, "pixelsLabel.text"); pixelHeightLabel = new JLabel(); localizer.setText(pixelHeightLabel, "heightLabel.text"); pixelHeightSpinner = createIntegerSpinner(6, 10); pixelHeightUnitLabel = new JLabel(); localizer.setText(pixelHeightUnitLabel, "pixelsLabel.text"); resolutionLabel = new JLabel(); localizer.setText(resolutionLabel, "resolutionLabel.text"); resolutionSpinner = createBigDecimalSpinner(4, 2, new BigDecimal(50)); resolutionUnitSelector = new DefaultSelector<JoglExportImageDialogModel.ResolutionUnit>(); resolutionUnitSelector.setAvailableValues(JoglExportImageDialogModel.ResolutionUnit.values()); printSizeHeaderLabel = new JLabel(); localizer.setText(printSizeHeaderLabel, "printSizeHeaderLabel.text"); printWidthLabel = new JLabel(); localizer.setText(printWidthLabel, "widthLabel.text"); printWidthSpinner = createBigDecimalSpinner(4, 2, new BigDecimal(1)); printUnitSelector = new DefaultSelector<JoglExportImageDialogModel.PrintSizeUnit>(); printUnitSelector.setAvailableValues(JoglExportImageDialogModel.PrintSizeUnit.values()); printHeightLabel = new JLabel(); localizer.setText(printHeightLabel, "heightLabel.text"); printHeightSpinner = createBigDecimalSpinner(4, 2, new BigDecimal(1)); printUnitLabel = new JLabel(); localizer.setText(printUnitLabel, "inches"); localizer.register(new Object(), new I18nUpdater<Object>() { @Override public void updateI18n(Localizer localizer, Object localizedObject) { JoglExportImageDialogModel.PrintSizeUnit.INCHES.displayName = localizer.getString("inches"); JoglExportImageDialogModel.PrintSizeUnit.CENTIMETERS.displayName = localizer.getString("centimeters"); JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_CM.displayName = localizer .getString("pixelsPerCm"); JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_IN.displayName = localizer .getString("pixelsPerIn"); printUnitSelector.comboBox().repaint(); resolutionUnitSelector.comboBox().repaint(); } }); numSamplesLabel = new JLabel(); localizer.setText(numSamplesLabel, "numSamplesLabel.text.off"); numSamplesSlider = new JSlider(1, 20, 1); numSamplesSlider.setPreferredSize(new Dimension(150, numSamplesSlider.getPreferredSize().height)); exportButton = new JButton(); localizer.setText(exportButton, "exportButton.text"); cancelButton = new JButton(); localizer.setText(cancelButton, "cancelButton.text"); } private JSpinner createIntegerSpinner(int maxDigits, int step) { BetterSpinnerNumberModel<Integer> model = BetterSpinnerNumberModel.newInstance(null, 1, (int) Math.floor(Math.pow(10, maxDigits)) - 1, step); JSpinner spinner = new JSpinner(model); SimpleSpinnerEditor editor = new SimpleSpinnerEditor(spinner); editor.getTextField().setColumns(maxDigits); Format<Integer> format = Formats.createIntegerFormat(maxDigits); SimpleFormatter formatter = new SimpleFormatter(format); formatter.install(editor.getTextField()); Pattern pattern = Patterns.createNumberPattern(maxDigits, 0, true); PatternDocumentFilter docFilter = new PatternDocumentFilter(pattern); ((PlainDocument) editor.getTextField().getDocument()).setDocumentFilter(docFilter); spinner.setEditor(editor); editor.getTextField().putClientProperty("value", spinner.getValue()); return spinner; } protected void createLayout() { outputFileOrWarningLabelHolder.setLayout(new BorderLayout()); outputFileOrWarningLabelHolder.add(outputFileOrWarningLabel, BorderLayout.CENTER); GridBagWizard gbw = GridBagWizard.quickPanel(); gbw.defaults().fillx(1.0).defaultAutoinsets(0, 10); GridBagWizard dirPanel = GridBagWizard.quickPanel(); dirPanel.defaults().west().defaultAutoinsets(0, 2); dirPanel.put(outputDirectoryLabel).xy(0, 0); dirPanel.put(outputDirectoryField).below(outputDirectoryLabel).fillboth(1.0, 0.0); dirPanel.put(chooseOutputDirectoryButton).rightOf(outputDirectoryField).filly(); gbw.put(dirPanel.getTarget()).xy(0, 0); GridBagWizard namePanel = GridBagWizard.quickPanel(); namePanel.defaults().west(); namePanel.defaults().defaultAutoinsets(2, 2); namePanel.put(fileNamePrefixLabel).xy(0, 0).addToInsets(0, 0, 0, 10); namePanel.put(fileNumberLabel).rightOf(fileNamePrefixLabel); namePanel.put(fileNamePrefixField).below(fileNamePrefixLabel).fillboth(1.0, 0.0); namePanel.put(fileNumberSpinner).below(fileNumberLabel).fillboth(); namePanel.put(outputFileOrWarningLabelHolder).below(fileNamePrefixField, fileNumberSpinner) .addToInsets(10, 0, 0, 0); gbw.put(namePanel.getTarget()).below(dirPanel.getTarget()); GridBagWizard sizePanel = GridBagWizard.quickPanel(); sizePanel.defaults().west().defaultAutoinsets(2, 2); sizePanel.put(pixelSizeHeaderLabel).xy(0, 0).width(3); sizePanel.put(new JSeparator()).below(pixelSizeHeaderLabel).fillx().ipady(3); sizePanel.put(pixelWidthLabel).belowLast().x(0).width(1); sizePanel.put(pixelWidthSpinner).rightOf(pixelWidthLabel); sizePanel.put(pixelWidthUnitLabel).rightOf(pixelWidthSpinner); sizePanel.put(pixelHeightLabel).below(pixelWidthLabel); sizePanel.put(pixelHeightSpinner).below(pixelWidthSpinner); sizePanel.put(pixelHeightUnitLabel).below(pixelWidthUnitLabel); sizePanel.put(resolutionLabel).below(pixelHeightLabel); sizePanel.put(resolutionSpinner).below(pixelHeightSpinner); sizePanel.put(resolutionUnitSelector.comboBox()).below(pixelHeightUnitLabel); sizePanel.put(pixelWidthLabel, pixelHeightLabel, resolutionLabel).addToInsets(0, 10, 0, 0); sizePanel.put(pixelWidthSpinner, pixelHeightSpinner, resolutionSpinner).fillboth(1.0, 0.0); sizePanel.put(resolutionUnitSelector.comboBox()).fillboth(); sizePanel.put(printSizeHeaderLabel).below(resolutionLabel).width(3).addToInsets(10, 0, 0, 0); sizePanel.put(new JSeparator()).below(printSizeHeaderLabel).fillx().ipady(3); sizePanel.put(printWidthLabel).belowLast().x(0).width(1); sizePanel.put(printWidthSpinner).rightOf(printWidthLabel); sizePanel.put(printUnitSelector.comboBox()).rightOf(printWidthSpinner); sizePanel.put(printHeightLabel).below(printWidthLabel); sizePanel.put(printHeightSpinner).below(printWidthSpinner); sizePanel.put(printUnitLabel).below(printUnitSelector.comboBox()); sizePanel.put(printWidthLabel, printHeightLabel).addToInsets(0, 10, 0, 0); sizePanel.put(printWidthSpinner, printHeightSpinner).fillboth(1.0, 0.0); sizePanel.put(printUnitSelector.comboBox()).fillboth(); sizePanel.put(pixelWidthUnitLabel, pixelHeightUnitLabel, printUnitLabel).addToInsets(0, 4, 0, 0); gbw.put(sizePanel.getTarget()).below(namePanel.getTarget()).addToInsets(10, 0, 0, 0); GridBagWizard numSamplesPanel = GridBagWizard.quickPanel(); numSamplesPanel.put(numSamplesLabel, numSamplesSlider).intoRow(); numSamplesPanel.put(numSamplesLabel).west().weightx(1.0).insets(0, 0, 0, 15); gbw.put(numSamplesPanel.getTarget()).belowLast().fillx(1.0).addToInsets(10, 0, 0, 0); GridBagWizard buttonPanel = GridBagWizard.quickPanel(); buttonPanel.defaults().defaultAutoinsets(2, 2); buttonPanel.put(exportButton, cancelButton).intoRow().fillx(1.0); gbw.put(buttonPanel.getTarget()).belowLast().south().fillx(1.0).weighty(1.0) .addToInsets(10, 0, 0, 0); JPanel leftPanel = (JPanel) gbw.getTarget(); leftPanel.setBorder(new CompoundBorder(new InnerGradientBorder(new Insets(0, 0, 0, 8), new Color(164, 164, 164)), new EmptyBorder(5, 5, 5, 2))); outputFileOrWarningLabelHolder.setPreferredSize(new Dimension(leftPanel.getPreferredSize().width, 40)); outputFileOrWarningLabelHolder.setMinimumSize(outputFileOrWarningLabelHolder.getPreferredSize()); leftPanel.setMinimumSize(leftPanel.getPreferredSize()); leftPanel.setPreferredSize(leftPanel.getPreferredSize()); leftPanel.setMaximumSize(leftPanel.getPreferredSize()); canvasHolder.setLayout(new CanvasHolderLayout()); getContentPane().add(leftPanel, BorderLayout.WEST); getContentPane().add(canvasHolder, BorderLayout.CENTER); } protected void createListeners() { resolutionUnitSelector.addSelectorListener(new ISelectorListener<JoglExportImageDialogModel.ResolutionUnit>() { @Override public void selectionChanged(ISelector<JoglExportImageDialogModel.ResolutionUnit> selector, JoglExportImageDialogModel.ResolutionUnit oldSelection, JoglExportImageDialogModel.ResolutionUnit newSelection) { if (updating || newSelection == null) { return; } updating = true; try { switch (newSelection) { case PIXELS_PER_IN: printUnitSelector.setSelection(JoglExportImageDialogModel.PrintSizeUnit.INCHES); if (oldSelection == JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_CM) { scaleUnits(CM_TO_IN); } break; case PIXELS_PER_CM: printUnitSelector.setSelection(JoglExportImageDialogModel.PrintSizeUnit.CENTIMETERS); if (oldSelection == JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_IN) { scaleUnits(IN_TO_CM); } break; default: break; } } finally { updating = false; } } }); printUnitSelector.addSelectorListener(new ISelectorListener<JoglExportImageDialogModel.PrintSizeUnit>() { @Override public void selectionChanged(ISelector<JoglExportImageDialogModel.PrintSizeUnit> selector, JoglExportImageDialogModel.PrintSizeUnit oldSelection, JoglExportImageDialogModel.PrintSizeUnit newSelection) { if (updating || newSelection == null) { return; } updating = true; try { switch (newSelection) { case INCHES: resolutionUnitSelector.setSelection(JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_IN); if (oldSelection == JoglExportImageDialogModel.PrintSizeUnit.CENTIMETERS) { scaleUnits(CM_TO_IN); } break; case CENTIMETERS: resolutionUnitSelector.setSelection(JoglExportImageDialogModel.ResolutionUnit.PIXELS_PER_CM); if (oldSelection == JoglExportImageDialogModel.PrintSizeUnit.INCHES) { scaleUnits(IN_TO_CM); } break; default: break; } printUnitLabel.setText(newSelection.toString()); } finally { updating = false; } } }); ChangeListener sizeChangeListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { if (updating) { return; } updating = true; try { if (e.getSource() == pixelWidthSpinner || e.getSource() == pixelHeightSpinner || e.getSource() == resolutionSpinner) { updatePrintSize(); } else if (e.getSource() == printWidthSpinner || e.getSource() == printHeightSpinner) { updatePixelSize(); } canvasHolder.invalidate(); canvasHolder.validate(); } finally { updating = false; } } }; pixelWidthSpinner.addChangeListener(sizeChangeListener); pixelHeightSpinner.addChangeListener(sizeChangeListener); resolutionSpinner.addChangeListener(sizeChangeListener); printWidthSpinner.addChangeListener(sizeChangeListener); printHeightSpinner.addChangeListener(sizeChangeListener); chooseOutputDirectoryButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { showOutputDirectoryChooser(); } }); class FileNameListener extends EasyDocumentListener implements ChangeListener { @Override public void documentChanged(DocumentEvent e) { handleFileNameChange(); } @Override public void stateChanged(ChangeEvent e) { handleFileNameChange(); } } FileNameListener fileNameListener = new FileNameListener(); fileNumberSpinner.addChangeListener(fileNameListener); outputDirectoryField.getDocument().addDocumentListener(fileNameListener); fileNamePrefixField.getDocument().addDocumentListener(fileNameListener); cancelButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dispose(); } }); exportButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { taskService.submit(new CaptureTask()); } }); numSamplesSlider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { updateNumSamplesLabel(); renderer.setDesiredNumSamples(numSamplesSlider.getValue()); if (canvas != null) { canvas.display(); } } }); } private File createOutputFile() throws LocalizedException { if (outputDirectoryField.getText() == null || "".equals(outputDirectoryField.getText())) { throw new LocalizedException("createOutputFile.exception.enterDirectory"); } File directory = null; try { directory = new File(outputDirectoryField.getText()); } catch (Exception ex) { throw new LocalizedException("createOutputFile.exception.invalidDirectory", outputDirectoryField.getText(), ex.getLocalizedMessage()); } if (fileNamePrefixField.getText() == null || "".equals(fileNamePrefixField.getText())) { throw new LocalizedException("createOutputFile.exception.enterFileNamePrefix"); } String fileName = fileNamePrefixField.getText(); if (fileNumberSpinner.getValue() instanceof Integer) { fileName += String.format("-%04d", fileNumberSpinner.getValue()); } fileName += ".png"; try { return new File(directory, fileName); } catch (Exception ex) { throw new LocalizedException("createOutputFile.exception.invalidFileNamePrefix", fileNamePrefixField.getText(), ex.getLocalizedMessage()); } } private void handleFileNameChange() { File outputFile = null; try { outputFile = createOutputFile(); localizer.unregister(outputFileOrWarningLabel); localizer.setFormattedText(outputFileOrWarningLabel, "fileNameExampleLabel.text", outputFile); outputFileOrWarningLabel.setIcon(null); } catch (LocalizedException ex) { localizer.unregister(outputFileOrWarningLabel); if (ex.getArgs() != null) { localizer.setFormattedText(outputFileOrWarningLabel, ex.getKey(), ex.getArgs()); } else { localizer.setText(outputFileOrWarningLabel, ex.getKey()); } outputFileOrWarningLabel.setIcon(warningIcon); } exportButton.setEnabled(outputFile != null); } protected void init(final I18n i18n) { new OnEDT() { @Override public void run() throws Throwable { setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); localizer = i18n.forClass(JoglExportImageDialog.class); setModalityType(ModalityType.DOCUMENT_MODAL); createComponents(); createLayout(); createListeners(); createBindings(); } }; taskService = new SingleThreadedTaskService(); } private void scaleUnits(BigDecimal factor) { BigDecimal resolution = (BigDecimal) resolutionSpinner.getValue(); if (resolution != null) { resolutionSpinner.setValue(resolution.divide(factor, RoundingMode.HALF_EVEN)); } BigDecimal printWidth = (BigDecimal) printWidthSpinner.getValue(); if (printWidth != null) { printWidthSpinner.setValue(printWidth.multiply(factor)); } BigDecimal printHeight = (BigDecimal) printHeightSpinner.getValue(); if (printHeight != null) { printHeightSpinner.setValue(printHeight.multiply(factor)); } } public void setBinder(Binder<QObject<JoglExportImageDialogModel>> binder) { this.binder.bind(binder); } public void setScene(JoglScene scene) { renderer.setScene(scene); } public void setViewSettings(JoglViewSettings viewSettings) { renderer.getViewSettings().copy(viewSettings); } @Override public void setVisible(boolean visible) { if (visible && canvas == null) { taskService.submit(new NEWTInitializer()); } super.setVisible(visible); } private void showOutputDirectoryChooser() { if (outputDirectoryChooser == null) { outputDirectoryChooser = new JFileChooser(); outputDirectoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); } File directory = null; try { directory = new File(outputDirectoryField.getText()); } catch (Exception ex) { } if (directory != null && directory.exists()) { outputDirectoryChooser.setCurrentDirectory(directory); } int choice = outputDirectoryChooser.showOpenDialog(this); if (choice == JFileChooser.APPROVE_OPTION) { directory = outputDirectoryChooser.getSelectedFile(); outputDirectoryField.setText(directory.getAbsolutePath()); } } private void updateNumSamplesLabel() { localizer.unregister(numSamplesLabel); int value = numSamplesSlider.getValue(); if (value <= 1) { localizer.setText(numSamplesLabel, "numSamplesLabel.text.off"); } else { localizer.setFormattedText(numSamplesLabel, "numSamplesLabel.text.on", value); } } private void updatePixelSize() { BigDecimal resolution = (BigDecimal) resolutionSpinner.getValue(); if (resolution == null) { return; } BigDecimal printWidth = (BigDecimal) printWidthSpinner.getValue(); if (printWidth != null && printWidth.compareTo(BigDecimal.ZERO) != 0) { pixelWidthSpinner.setValue(resolution.multiply(printWidth).intValue()); } else { pixelWidthSpinner.setValue(null); } BigDecimal printHeight = (BigDecimal) printHeightSpinner.getValue(); if (printHeight != null && printHeight.compareTo(BigDecimal.ZERO) != 0) { pixelHeightSpinner.setValue(resolution.multiply(printHeight).intValue()); } else { pixelHeightSpinner.setValue(null); } } private void updatePrintSize() { BigDecimal resolution = (BigDecimal) resolutionSpinner.getValue(); if (resolution == null) { return; } Integer pixelWidth = (Integer) pixelWidthSpinner.getValue(); if (pixelWidth != null && pixelWidth != 0) { printWidthSpinner.setValue(new BigDecimal(pixelWidth).divide(resolution, 2, BigDecimal.ROUND_HALF_EVEN)); } else { printWidthSpinner.setValue(null); } Integer pixelHeight = (Integer) pixelHeightSpinner.getValue(); if (pixelHeight != null && pixelHeight != 0) { printHeightSpinner.setValue(new BigDecimal(pixelHeight).divide(resolution, 2, BigDecimal.ROUND_HALF_EVEN)); } else { printWidthSpinner.setValue(null); } } }