/*
* GRAL: GRAphing Library for Java(R)
*
* (C) Copyright 2009-2016 Erich Seifert ,
* Michael Seifert
*
* This file is part of GRAL.
*
* GRAL is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GRAL 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with GRAL. If not, see .
*/
package de.erichseifert.gral.ui;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import de.erichseifert.gral.graphics.Container;
import de.erichseifert.gral.graphics.Drawable;
import de.erichseifert.gral.graphics.DrawingContext;
import de.erichseifert.gral.io.IOCapabilities;
import de.erichseifert.gral.io.plots.DrawableWriter;
import de.erichseifert.gral.io.plots.DrawableWriterFactory;
import de.erichseifert.gral.navigation.Navigable;
import de.erichseifert.gral.navigation.Navigator;
import de.erichseifert.gral.util.Messages;
import de.erichseifert.gral.util.PointND;
/**
* A panel implementation that displays a {@code Drawable} instance as a
* rich Swing component.
*/
public class InteractivePanel extends DrawablePanel implements Printable {
/** Version id for serialization. */
private static final long serialVersionUID = 9084883142053148090L;
// FIXME Find better method to adjust resolution
/** Constant that can be used to convert from millimeters to points
(1/72 inch). */
private static final double MM_TO_PT = 72.0/25.4;
/** Constant that defines how many millimeters a pixel will be. */
private static final double MM_PER_PX = 0.2*MM_TO_PT;
/** Job for printing the current panel. */
private final PrinterJob printerJob;
/** Value that is necessary before panning is triggered. */
private static final int MIN_DRAG = 0;
/** Defines whether the panel can be zoomed. */
private boolean zoomable;
/** Defines whether the panel can be panned. */
private boolean pannable;
/** Map that stored actions by names like "zoomIn", "zoomOut",
"resetView", "exportImage", or "print". */
protected final ActionMap actions;
/** Cache for the popup menu. */
private JPopupMenu popupMenu;
private boolean popupMenuEnabled;
private Point2D popupMenuPos;
/** Chooser for image export. */
private final JFileChooser exportImageChooser;
/** Object to be used as listener for zooming actions. */
private MouseZoomListener zoomListener;
/** Object to be used as listener for panning actions. */
private NavigationMoveListener panListener;
/**
* Listener class for zooming actions.
*/
private final static class MouseZoomListener extends MouseAdapter
implements MouseWheelListener, Serializable {
/** Version id for serialization. */
private static final long serialVersionUID = -7323541053291673122L;
private final InteractivePanel panel;
public MouseZoomListener(InteractivePanel panel) {
this.panel = panel;
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
Point2D point = e.getPoint();
panel.zoom(point, -e.getWheelRotation());
}
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e) &&
(e.getClickCount() == 2)) {
Point2D point = e.getPoint();
panel.zoom(point, 1);
}
}
}
/**
* Creates a new panel instance and initializes it with a
* drawable component.
* @param drawable Drawable component.
*/
@SuppressWarnings("serial")
public InteractivePanel(Drawable drawable) {
super(drawable);
printerJob = PrinterJob.getPrinterJob();
printerJob.setPrintable(this);
List exportFormats = DrawableWriterFactory.getInstance()
.getCapabilities();
exportImageChooser = new ExportChooser(true, exportFormats);
exportImageChooser.setDialogTitle(Messages.getString(
"InteractivePanel.exportImageTitle")); //$NON-NLS-1$
actions = new ActionMap();
actions.put("zoomIn", new AbstractAction(Messages.getString( //$NON-NLS-1$
"InteractivePanel.zoomIn")) { //$NON-NLS-1$
public void actionPerformed(ActionEvent e) {
zoom(popupMenuPos, 1);
}
});
actions.put("zoomOut", new AbstractAction(Messages.getString( //$NON-NLS-1$
"InteractivePanel.zoomOut")) { //$NON-NLS-1$
public void actionPerformed(ActionEvent e) {
zoom(popupMenuPos, -1);
}
});
actions.put("resetView", new AbstractAction(Messages.getString( //$NON-NLS-1$
"InteractivePanel.resetView")) { //$NON-NLS-1$
public void actionPerformed(ActionEvent e) {
resetZoom(popupMenuPos);
}
});
actions.put("exportImage", new AbstractAction(Messages.getString( //$NON-NLS-1$
"InteractivePanel.exportImage")) { //$NON-NLS-1$
public void actionPerformed(ActionEvent e) {
int ret = exportImageChooser.showSaveDialog(
InteractivePanel.this);
// Clear artifacts of the file chooser
repaint();
// If the user aborted we can stop
if (ret != JFileChooser.APPROVE_OPTION) {
return;
}
// If the user didn't select a file we can stop
File file = exportImageChooser.getSelectedFile();
if (file == null) {
return;
}
// If the selected an existing file we ask for permission
// to overwrite it
else if (file.exists()) {
int retOverwrite = JOptionPane.showConfirmDialog(
InteractivePanel.this,
Messages.getString("InteractivePanel.exportExistsWarning"), //$NON-NLS-1$
Messages.getString("InteractivePanel.warning"), //$NON-NLS-1$
JOptionPane.YES_NO_OPTION
);
// Clear artifacts of the confirm dialog
repaint();
if (retOverwrite == JOptionPane.NO_OPTION) {
return;
}
}
// Export current view to the selected file
Drawable d = getDrawable();
ExportDialog ed = new ExportDialog(InteractivePanel.this, d);
ed.setVisible(true);
if (!ed.getUserAction().equals(
ExportDialog.UserAction.APPROVE)) {
return;
}
DrawableWriterFilter filter = (DrawableWriterFilter)
exportImageChooser.getFileFilter();
export(d, filter.getWriterCapabilities().getMimeType(),
file, ed.getDocumentBounds());
}
});
actions.put("print", new AbstractAction(Messages.getString( //$NON-NLS-1$
"InteractivePanel.print")) { //$NON-NLS-1$
public void actionPerformed(ActionEvent e) {
if (printerJob.printDialog()) {
try {
printerJob.print();
} catch (PrinterException ex) {
// TODO Show error dialog
ex.printStackTrace();
}
}
}
});
popupMenuEnabled = true;
addMouseListener(new PopupListener());
setZoomable(true);
setPannable(true);
}
/**
* Method that returns the popup menu for a given mouse event. It will be
* called on each popup event if the menu is enabled. If the menu is static
* caching can be used to prevent unnecessary generation of menu objects.
* @param e Mouse event that triggered the popup menu.
* @return A popup menu instance, or {@code null} if no popup menu should be shown.
* @see #isPopupMenuEnabled()
* @see #setPopupMenuEnabled(boolean)
*/
protected JPopupMenu getPopupMenu(MouseEvent e) {
if (popupMenu == null) {
popupMenu = new JPopupMenu();
popupMenu.add(actions.get("zoomIn")); //$NON-NLS-1$
popupMenu.add(actions.get("zoomOut")); //$NON-NLS-1$
popupMenu.add(actions.get("resetView")); //$NON-NLS-1$
popupMenu.addSeparator();
popupMenu.add(actions.get("exportImage")); //$NON-NLS-1$
popupMenu.add(actions.get("print")); //$NON-NLS-1$
}
return popupMenu;
}
/**
* Returns whether a popup menu will be shown by this panel when the user
* takes the appropriate action. The necessary action depends on the
* operating system of the user.
* @return {@code true} when a popup menu will be shown,
* otherwise {@code false}.
*/
public boolean isPopupMenuEnabled() {
return popupMenuEnabled;
}
/**
* Sets whether a popup menu will be shown by this panel when the user
* takes the appropriate action. The necessary action depends on the
* operating system of the user.
* @param popupMenuEnabled {@code true} when a popup menu should be
* shown, otherwise {@code false}.
*/
public void setPopupMenuEnabled(boolean popupMenuEnabled) {
this.popupMenuEnabled = popupMenuEnabled;
}
/**
* Zooms a navigable object in (positive values) or out (negative values).
* @param point The location where the zoom was triggered.
* @param times Number of times the navigable object will be zoomed.
* Positive values zoom in, negative values zoom out.
*/
private void zoom(Point2D point, int times) {
if (!isZoomable()) {
return;
}
Navigable navigable = InteractivePanel.getNavigableAt(getDrawable(), point);
if (navigable == null) {
return;
}
Navigator navigator = navigable.getNavigator();
if (times >= 0) {
for (int i = 0; i < times; i++) {
navigator.zoomIn();
}
} else {
for (int i = 0; i < -times; i++) {
navigator.zoomOut();
}
}
repaint();
}
private void resetZoom(Point2D point) {
if (!isZoomable()) {
return;
}
Navigable navigable = InteractivePanel.getNavigableAt(getDrawable(), point);
if (navigable == null) {
return;
}
Navigator navigator = navigable.getNavigator();
navigator.reset();
repaint();
}
/**
* Method that exports the current view to a file using a specified file type.
* @param component Drawable that will be exported.
* @param mimeType File format as MIME type string.
* @param file File to export to.
* @param documentBounds Document boundary rectangle
*/
private void export(Drawable component, String mimeType, File file,
Rectangle2D documentBounds) {
FileOutputStream destination;
try {
destination = new FileOutputStream(file);
} catch (FileNotFoundException ex) {
// TODO Auto-generated catch block
ex.printStackTrace();
return;
}
DrawableWriter writer = DrawableWriterFactory.getInstance().get(mimeType);
try {
writer.write(component, destination,
documentBounds.getX(), documentBounds.getY(),
documentBounds.getWidth(), documentBounds.getHeight());
} catch (IOException ex) {
// TODO Auto-generated catch block
ex.printStackTrace();
} finally {
try {
destination.close();
} catch (IOException ex2) {
// TODO Auto-generated catch block
ex2.printStackTrace();
}
}
}
/**
* Class that is responsible for showing the popup menu.
*/
private class PopupListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
showPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
showPopup(e);
}
private void showPopup(MouseEvent e) {
if (!isPopupMenuEnabled() || !e.isPopupTrigger()) {
return;
}
JPopupMenu menu = getPopupMenu(e);
if (menu == null) {
return;
}
popupMenuPos = e.getPoint();
menu.show(e.getComponent(), e.getX(), e.getY());
}
}
/**
* Class that handles mouse moves for navigation.
*/
private static class NavigationMoveListener extends MouseAdapter {
/** A reference to the panel for refreshing. */
private final InteractivePanel panel;
/** AbstractPlot that will be changed by this class. */
private Navigable navigable;
/** Previously clicked point or {@code null}. */
private Point posPrev;
/**
* Creates a new listener and initializes it with a panel.
* @param panel InteractivePanel that should be refreshed.
*/
public NavigationMoveListener(InteractivePanel panel) {
this.panel = panel;
}
@Override
public void mousePressed(MouseEvent e) {
Point point = e.getPoint();
navigable = InteractivePanel.getNavigableAt(panel.getDrawable(), point);
posPrev = point;
}
@Override
public void mouseDragged(MouseEvent e) {
if (navigable == null) {
return;
}
// Calculate distance that the current view was dragged
// (screen units)
Point pos = e.getPoint();
Navigator navigator = navigable.getNavigator();
int dx = pos.x - posPrev.x;
int dy = pos.y - posPrev.y;
posPrev = pos;
if (Math.abs(dx) > MIN_DRAG || Math.abs(dy) > MIN_DRAG) {
PointND deltas = new PointND(dx, dy);
navigator.pan(deltas);
panel.repaint();
}
}
}
/**
* Prints the page at the specified index into the specified
* {@link Graphics} context in the specified format.
* @param g the context into which the page is drawn
* @param pageFormat the size and orientation of the page being drawn
* @param pageIndex the zero based index of the page to be drawn
* @return PAGE_EXISTS if the page is rendered successfully
* or NO_SUCH_PAGE if {@code pageIndex} specifies a
* non-existent page.
* @exception java.awt.print.PrinterException
* thrown when the print job is terminated.
*/
public int print(Graphics g, PageFormat pageFormat, int pageIndex)
throws PrinterException {
if (pageIndex > 0) {
return Printable.NO_SUCH_PAGE;
}
Graphics2D graphics = (Graphics2D) g;
AffineTransform txOld = graphics.getTransform();
graphics.scale(MM_PER_PX, MM_PER_PX);
Rectangle2D boundsOld = getDrawable().getBounds();
Rectangle2D pageBounds = new Rectangle2D.Double(
pageFormat.getImageableX()/MM_PER_PX,
pageFormat.getImageableY()/MM_PER_PX,
pageFormat.getImageableWidth()/MM_PER_PX,
pageFormat.getImageableHeight()/MM_PER_PX
);
// Set size
// TODO Keep Drawable's aspect ratio when scaling
getDrawable().setBounds(pageBounds);
// TODO Assure to temporarily turn off anti-aliasing before printing
try {
getDrawable().draw(new DrawingContext(graphics));
} finally {
getDrawable().setBounds(boundsOld);
}
graphics.setTransform(txOld);
return Printable.PAGE_EXISTS;
}
/**
* Returns whether the plot area in the panel can be zoomed.
* @return {@code true} if the plot can be zoomed,
* {@code false} otherwise.
*/
public boolean isZoomable() {
return zoomable;
}
/**
* Sets whether the plot area in the panel can be zoomed.
* @param zoomable {@code true} if the plot should be zoomable,
* {@code false} otherwise.
*/
public void setZoomable(boolean zoomable) {
if (this.zoomable == zoomable) {
return;
}
this.zoomable = zoomable;
if (zoomListener != null) {
removeMouseWheelListener(zoomListener);
removeMouseListener(zoomListener);
zoomListener = null;
}
if (zoomable) {
zoomListener = new MouseZoomListener(this);
addMouseListener(zoomListener);
addMouseWheelListener(zoomListener);
}
actions.get("zoomIn").setEnabled(isZoomable()); //$NON-NLS-1$
actions.get("zoomOut").setEnabled(isZoomable()); //$NON-NLS-1$
actions.get("resetView").setEnabled(isZoomable() && isPannable()); //$NON-NLS-1$
}
/**
* Returns whether the plot area in the panel can be panned.
* @return {@code true} if the plot can be panned,
* {@code false} otherwise.
*/
public boolean isPannable() {
return pannable;
}
/**
* Sets whether the plot area in the panel can be panned.
* @param pannable {@code true} if the plot should be pannable,
* {@code false} otherwise.
*/
public void setPannable(boolean pannable) {
if (this.pannable == pannable) {
return;
}
this.pannable = pannable;
if (panListener != null) {
removeMouseMotionListener(panListener);
removeMouseListener(panListener);
panListener = null;
}
if (pannable) {
// Register a new handler to move the map by dragging
// This requires that an x- and a y-axis do exist in the plot
panListener = new NavigationMoveListener(this);
addMouseListener(panListener);
addMouseMotionListener(panListener);
}
actions.get("resetView").setEnabled(isZoomable() && isPannable()); //$NON-NLS-1$
}
/**
* Returns a navigable area at the specified point, {@code null} if no
* object could be found. If the specified container isn't navigable, its
* children are recursively checked.
* @param drawable The drawable container to check for navigable children.
* @param point Position that should hit the navigable object.
* @return A navigable object.
*/
private static Navigable getNavigableAt(Drawable drawable, Point2D point) {
List componentsToCheck;
if (drawable instanceof Container) {
componentsToCheck = ((Container) drawable).getDrawablesAt(point);
} else {
componentsToCheck = new ArrayList(1);
componentsToCheck.add(drawable);
}
for (Drawable component : componentsToCheck) {
if ((component instanceof Navigable) && component.getBounds().contains(point)) {
return (Navigable) component;
}
}
return null;
}
}
Ads help maintain this website.