// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.print;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.print.PageFormat;
import java.awt.print.PrinterAbortException;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.Attribute;
import javax.print.attribute.EnumSyntax;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.HashPrintServiceAttributeSet;
import javax.print.attribute.IntegerSyntax;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.PrintServiceAttribute;
import javax.print.attribute.TextSyntax;
import javax.print.attribute.standard.Media;
import javax.print.attribute.standard.MediaPrintableArea;
import javax.print.attribute.standard.OrientationRequested;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.openstreetmap.gui.jmapviewer.tilesources.AbstractOsmTileSource;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.Utils;
import org.openstreetmap.josm.tools.WindowGeometry;
/**
* A print dialog with preview
* @author Kai Pastor
*/
public class PrintDialog extends JDialog implements ActionListener {
/**
* The printer name
*/
protected JTextField printerField;
/**
* The media format name
*/
protected JTextField paperField;
/**
* The media orientation
*/
protected JTextField orientationField;
/**
* The preview toggle checkbox
*/
protected JCheckBox previewCheckBox;
/**
* The resolution in dpi for printing/preview
*/
protected SpinnerNumberModel resolutionModel;
/**
* The map scale
*/
protected SpinnerNumberModel scaleModel;
/**
* The page preview
*/
protected PrintPreview printPreview;
/**
* The map view for preview an printing
*/
protected PrintableMapView mapView;
/**
* The printer job
*/
protected transient PrinterJob job;
/**
* The custom printer job attributes
*/
transient PrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
/**
* Create a new print dialog
*
* @param parent the parent component
*/
public PrintDialog(Component parent) {
super(JOptionPane.getFrameForComponent(parent), tr("Print the Map"), ModalityType.DOCUMENT_MODAL);
mapView = new PrintableMapView();
job = PrinterJob.getPrinterJob();
job.setJobName("JOSM Map");
job.setPrintable(mapView);
build();
loadPrintSettings();
updateFields();
pack();
setMaximumSize(Toolkit.getDefaultToolkit().getScreenSize());
}
/**
* Show or hide the dialog
*
* Set the dialog size to reasonable values.
*
* @param visible a flag indication the visibility of the dialog
*/
@Override
public void setVisible(boolean visible) {
if (visible) {
// Make the dialog at most as large as the parent JOSM window
// Have to take window decorations into account or the windows will
// be too large
Insets i = this.getParent().getInsets();
Dimension p = this.getParent().getSize();
p = new Dimension(Math.min(p.width-i.left-i.right, 1000),
Math.min(p.height-i.top-i.bottom, 700));
new WindowGeometry(
getClass().getName() + ".geometry",
WindowGeometry.centerInWindow(
getParent(),
p
)
).applySafe(this);
} else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
new WindowGeometry(this).remember(getClass().getName() + ".geometry");
Main.pref.put("print.preview.enabled", previewCheckBox.isSelected());
}
super.setVisible(visible);
}
/**
* Construct the dialog from components
*/
public void build() {
setLayout(new GridBagLayout());
final GBC std = GBC.std().insets(0, 5, 5, 0);
std.fill = GBC.HORIZONTAL;
final GBC twoColumns = GBC.std().insets(0, 5, 5, 0).span(2);
twoColumns.fill = GBC.HORIZONTAL;
final GBC threeColumns = GBC.std().insets(0, 5, 5, 0).span(3);
threeColumns.fill = GBC.HORIZONTAL;
JLabel caption;
int row = 0;
caption = new JLabel(tr("Printer")+":");
add(caption, twoColumns.grid(2, row));
printerField = new JTextField();
printerField.setEditable(false);
add(printerField, std.grid(GBC.RELATIVE, row));
row++;
caption = new JLabel(tr("Media")+":");
add(caption, twoColumns.grid(2, row));
paperField = new JTextField();
paperField.setEditable(false);
add(paperField, std.grid(GBC.RELATIVE, row));
row++;
caption = new JLabel(tr("Orientation")+":");
add(caption, twoColumns.grid(2, row));
orientationField = new JTextField();
orientationField.setEditable(false);
add(orientationField, std.grid(GBC.RELATIVE, row));
row++;
JButton printerButton = new JButton(tr("Printer settings")+"...");
printerButton.setActionCommand("printer-dialog");
printerButton.addActionListener(this);
add(printerButton, threeColumns.grid(2, row));
row++;
add(GBC.glue(5, 10), GBC.std(1, row).fill(GBC.VERTICAL));
row++;
caption = new JLabel(tr("Scale")+":");
add(caption, std.grid(2, row));
caption = new JLabel(" 1 :");
add(caption, std.grid(GBC.RELATIVE, row));
int mapScale = Main.pref.getInteger("print.map-scale", PrintPlugin.DEF_MAP_SCALE);
mapView.setFixedMapScale(mapScale);
scaleModel = new SpinnerNumberModel(mapScale, 250, 5000000, 250);
final JSpinner scaleField = new JSpinner(scaleModel);
scaleField.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent evt) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
scaleField.commitEdit();
Main.pref.put("print.map-scale", scaleModel.getNumber().toString());
mapView.setFixedMapScale(scaleModel.getNumber().intValue());
printPreview.repaint();
} catch (ParseException e) {
Main.error(e);
}
}
});
}
});
add(scaleField, std.grid(GBC.RELATIVE, row));
row++;
caption = new JLabel(tr("Resolution")+":");
add(caption, std.grid(2, row));
caption = new JLabel("ppi");
add(caption, std.grid(GBC.RELATIVE, row));
resolutionModel = new SpinnerNumberModel(
Main.pref.getInteger("print.resolution.dpi", PrintPlugin.DEF_RESOLUTION_DPI),
30, 1200, 10);
final JSpinner resolutionField = new JSpinner(resolutionModel);
resolutionField.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent evt) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
resolutionField.commitEdit();
Main.pref.put("print.resolution.dpi", resolutionModel.getNumber().toString());
printPreview.repaint();
} catch (ParseException e) {
Main.error(e);
}
}
});
}
});
add(resolutionField, std.grid(GBC.RELATIVE, row));
row++;
caption = new JLabel(tr("Map information")+":");
add(caption, threeColumns.grid(2, row));
row++;
final JTextArea attributionText = new JTextArea(Main.pref.get("print.attribution", AbstractOsmTileSource.DEFAULT_OSM_ATTRIBUTION));
attributionText.setRows(10);
attributionText.setLineWrap(true);
attributionText.setWrapStyleWord(true);
attributionText.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent evt) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Main.pref.put("print.attribution", attributionText.getText());
printPreview.repaint();
}
});
}
@Override
public void removeUpdate(DocumentEvent evt) {
this.insertUpdate(evt);
}
@Override
public void changedUpdate(DocumentEvent evt) {
// NOP
}
});
JScrollPane attributionPane = new JScrollPane(attributionText);
add(attributionPane, GBC.std().insets(0, 5, 5, 0).span(3).fill(GBC.BOTH).weight(0.0, 1.0).grid(2, row));
row++;
add(GBC.glue(5, 10), GBC.std(1, row).fill(GBC.VERTICAL));
row++;
previewCheckBox = new JCheckBox(tr("Map Preview"));
previewCheckBox.setSelected(Main.pref.getBoolean("print.preview.enabled", false));
previewCheckBox.setActionCommand("toggle-preview");
previewCheckBox.addActionListener(this);
add(previewCheckBox, threeColumns.grid(2, row));
row++;
JButton zoomInButton = new JButton(tr("Zoom In"));
zoomInButton.setActionCommand("zoom-in");
zoomInButton.addActionListener(this);
add(zoomInButton, threeColumns.grid(2, row));
row++;
JButton zoomOutButton = new JButton(tr("Zoom Out"));
zoomOutButton.setActionCommand("zoom-out");
zoomOutButton.addActionListener(this);
add(zoomOutButton, threeColumns.grid(2, row));
row++;
JButton zoomToPageButton = new JButton(tr("Zoom To Page"));
zoomToPageButton.setActionCommand("zoom-to-page");
zoomToPageButton.addActionListener(this);
add(zoomToPageButton, threeColumns.grid(2, row));
row++;
JButton zoomToActualSize = new JButton(tr("Zoom To Actual Size"));
zoomToActualSize.setActionCommand("zoom-to-actual-size");
zoomToActualSize.addActionListener(this);
add(zoomToActualSize, threeColumns.grid(2, row));
printPreview = new PrintPreview();
if (previewCheckBox.isSelected()) {
printPreview.setPrintable(mapView);
}
JScrollPane previewPane = new JScrollPane(printPreview,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
previewPane.setPreferredSize(Main.main != null ? Main.map.mapView.getSize() : new Dimension(210, 297));
add(previewPane, GBC.std(0, 0).span(1, GBC.RELATIVE).fill().weight(5.0, 5.0));
row++;
JPanel actionPanel = new JPanel();
JButton printButton = new JButton(tr("Print"));
printButton.setActionCommand("print");
printButton.addActionListener(this);
actionPanel.add(printButton);
JButton cancelButton = new JButton(tr("Cancel"));
cancelButton.setActionCommand("cancel");
cancelButton.addActionListener(this);
actionPanel.add(cancelButton);
add(actionPanel, GBC.std(0, row).insets(5, 5, 5, 5).span(GBC.REMAINDER).fill(GBC.HORIZONTAL));
}
/**
* Update the dialog fields from the underlying model
*/
protected void updateFields() {
PrintService service = job.getPrintService();
if (service == null) {
printerField.setText("-");
paperField.setText("-");
orientationField.setText("-");
} else {
printerField.setText(service.getName());
if (!attrs.containsKey(Media.class)) {
attrs.add((Attribute) service.getDefaultAttributeValue(Media.class));
}
if (attrs.containsKey(Media.class)) {
paperField.setText(attrs.get(Media.class).toString());
}
if (!attrs.containsKey(OrientationRequested.class)) {
attrs.add((Attribute) service.getDefaultAttributeValue(OrientationRequested.class));
}
if (attrs.containsKey(OrientationRequested.class)) {
orientationField.setText(attrs.get(OrientationRequested.class).toString());
}
if (!attrs.containsKey(MediaPrintableArea.class)) {
PageFormat pf = job.defaultPage();
attrs.add(new MediaPrintableArea(
(float) pf.getImageableX()/72f,
(float) pf.getImageableY()/72f,
(float) pf.getImageableWidth()/72f,
(float) pf.getImageableHeight()/72f,
MediaPrintableArea.INCH));
}
PageFormat pf = job.getPageFormat(attrs);
printPreview.setPageFormat(pf);
}
}
/**
* Handle user input
*
* @param e an ActionEvent with one of the known commands
*/
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if ("printer-dialog".equals(cmd)) {
if (job.printDialog(attrs)) {
updateFields();
savePrintSettings();
}
} else if ("toggle-preview".equals(cmd)) {
Main.pref.put("print.preview.enabled", previewCheckBox.isSelected());
if (previewCheckBox.isSelected()) {
printPreview.setPrintable(mapView);
} else {
printPreview.setPrintable(null);
}
} else if ("zoom-in".equals(cmd)) {
printPreview.zoomIn();
} else if ("zoom-out".equals(cmd)) {
printPreview.zoomOut();
} else if ("zoom-to-page".equals(cmd)) {
printPreview.zoomToPage();
} else if ("zoom-to-actual-size".equals(cmd)) {
printPreview.setZoom(1.0);
} else if ("print".equals(cmd)) {
try {
job.print(attrs);
} catch (PrinterAbortException ex) {
String msg = ex.getLocalizedMessage();
if (msg.length() == 0) {
msg = tr("Printing has been cancelled.");
}
JOptionPane.showMessageDialog(Main.parent, msg,
tr("Printing stopped"),
JOptionPane.WARNING_MESSAGE);
} catch (PrinterException ex) {
String msg = ex.getLocalizedMessage();
if (msg == null || msg.length() == 0) {
msg = tr("Printing has failed.");
}
JOptionPane.showMessageDialog(Main.parent, msg,
tr("Printing stopped"),
JOptionPane.ERROR_MESSAGE);
}
dispose();
} else if ("cancel".equals(cmd)) {
dispose();
}
}
protected void savePrintSettings() {
// Save only one printer service attribute: printer name
PrintService service = job.getPrintService();
if (service != null) {
Collection<Collection<String>> serviceAttributes = new ArrayList<>();
for (Attribute a : service.getAttributes().toArray()) {
if ("printer-name".equals(a.getName()) && a instanceof TextSyntax) {
serviceAttributes.add(marshallPrintSetting(a, TextSyntax.class, ((TextSyntax) a).getValue()));
}
}
Main.pref.putArray("print.settings.service-attributes", serviceAttributes);
}
// Save all request attributes
Collection<String> ignoredAttributes = Arrays.asList("media-printable-area");
Collection<Collection<String>> requestAttributes = new ArrayList<>();
for (Attribute a : attrs.toArray()) {
Collection<String> setting = null;
if (a instanceof TextSyntax) {
setting = marshallPrintSetting(a, TextSyntax.class, ((TextSyntax) a).getValue());
} else if (a instanceof EnumSyntax) {
setting = marshallPrintSetting(a, EnumSyntax.class, Integer.toString(((EnumSyntax) a).getValue()));
} else if (a instanceof IntegerSyntax) {
setting = marshallPrintSetting(a, IntegerSyntax.class, Integer.toString(((IntegerSyntax) a).getValue()));
} else if (!ignoredAttributes.contains(a.getName())) {
// TODO: Add support for DateTimeSyntax, SetOfIntegerSyntax, ResolutionSyntax if needed
Main.warn("Print request attribute not supported: "+a.getName() +": "+a.getCategory()+" - "+a.toString());
}
if (setting != null) {
requestAttributes.add(setting);
}
}
Main.pref.putArray("print.settings.request-attributes", requestAttributes);
}
protected Collection<String> marshallPrintSetting(Attribute a, Class<?> syntaxClass, String value) {
return new ArrayList<>(Arrays.asList(a.getCategory().getName(), a.getClass().getName(), syntaxClass.getName(), value));
}
@SuppressWarnings("unchecked")
static Attribute unmarshallPrintSetting(Collection<String> setting) throws
ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
if (setting == null || setting.size() != 4) {
throw new IllegalArgumentException("Invalid setting: "+setting);
}
Iterator<String> it = setting.iterator();
Class<? extends Attribute> category = (Class<? extends Attribute>) Class.forName(it.next());
Class<? extends Attribute> realClass = (Class<? extends Attribute>) Class.forName(it.next());
Class<?> syntax = Class.forName(it.next());
String value = it.next();
if (syntax.equals(TextSyntax.class)) {
return realClass.getConstructor(String.class, Locale.class).newInstance(value, null);
} else if (syntax.equals(EnumSyntax.class)) {
int intValue = Integer.parseInt(value);
// First method - access static enum fields by reflection for classes that do not implement getEnumValueTable
for (Field f : realClass.getFields()) {
if (Modifier.isStatic(f.getModifiers()) && category.isAssignableFrom(f.getType())) {
EnumSyntax es = (EnumSyntax) f.get(null);
if (es.getValue() == intValue) {
return (Attribute) es;
}
}
}
// Second method - get instance from getEnumValueTable by reflection
try {
Method getEnumValueTable = realClass.getDeclaredMethod("getEnumValueTable");
Constructor<? extends Attribute> constructor = realClass.getDeclaredConstructor(int.class);
Utils.setObjectsAccessible(getEnumValueTable, constructor);
Attribute fakeInstance = constructor.newInstance(Integer.MAX_VALUE);
EnumSyntax[] enumTable = (EnumSyntax[]) getEnumValueTable.invoke(fakeInstance);
return (Attribute) enumTable[intValue];
} catch (ReflectiveOperationException | ArrayIndexOutOfBoundsException e) {
throw new IllegalArgumentException("Invalid enum: "+realClass+" - "+value, e);
}
} else if (syntax.equals(IntegerSyntax.class)) {
return realClass.getConstructor(int.class).newInstance(Integer.parseInt(value));
} else {
Main.warn("Attribute syntax not supported: "+syntax);
}
return null;
}
protected void loadPrintSettings() {
for (Collection<String> setting : Main.pref.getArray("print.settings.service-attributes")) {
try {
PrintServiceAttribute a = (PrintServiceAttribute) unmarshallPrintSetting(setting);
if ("printer-name".equals(a.getName())) {
job.setPrintService(PrintServiceLookup.lookupPrintServices(null, new HashPrintServiceAttributeSet(a))[0]);
}
} catch (PrinterException | ReflectiveOperationException e) {
Main.warn(e.getClass().getSimpleName()+": "+e.getMessage());
}
}
for (Collection<String> setting : Main.pref.getArray("print.settings.request-attributes")) {
try {
attrs.add(unmarshallPrintSetting(setting));
} catch (ReflectiveOperationException e) {
Main.warn(e.getClass().getSimpleName()+": "+e.getMessage());
}
}
}
}