/* * $Id$ * This file is a part of the Arakhne Foundation Classes, http://www.arakhne.org/afc * * Copyright (c) 2000-2012 Stephane GALLAND. * Copyright (c) 2005-10, Multiagent Team, Laboratoire Systemes et Transports, * Universite de Technologie de Belfort-Montbeliard. * Copyright (c) 2013-2016 The original authors, and other authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.arakhne.afc.ui.swing.zoom; import java.awt.Adjustable; import java.awt.BorderLayout; import java.awt.Color; import java.awt.ComponentOrientation; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.awt.print.PageFormat; import java.awt.print.Printable; import java.awt.print.PrinterException; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.BoxLayout; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputListener; import org.arakhne.afc.math.continous.object2d.Circle2f; import org.arakhne.afc.math.continous.object2d.Ellipse2f; import org.arakhne.afc.math.continous.object2d.PathIterator2f; import org.arakhne.afc.math.continous.object2d.Point2f; import org.arakhne.afc.math.continous.object2d.Rectangle2f; import org.arakhne.afc.math.continous.object2d.RoundRectangle2f; import org.arakhne.afc.math.continous.object2d.Segment2f; import org.arakhne.afc.math.continous.object2d.Shape2f; import org.arakhne.afc.ui.CenteringTransform; import org.arakhne.afc.ui.Graphics2DLOD; import org.arakhne.afc.ui.ZoomableContext; import org.arakhne.afc.ui.ZoomableContextUtil; import org.arakhne.afc.ui.event.KeyEvent; import org.arakhne.afc.ui.event.PointerEvent; import org.arakhne.afc.ui.swing.event.KeyEventSwing; import org.arakhne.afc.ui.swing.event.PointerEventSwing; import org.arakhne.afc.ui.swing.zoom.ScrollingMethod.ScrollingMethodListener; /** This abstract view provides the tools to move and scale * the view. It is abstract because it does not draw anything. * <p> * The implementation of the ZoomableView handles the pointer as following: * <table border="1" width="100%" summary=""> * <thead> * <tr><td>Event</td><td>Status</td><td>Callback</td><td>Note</td></tr> * </thead> * <tbody> * <tr><td>POINTER_PRESSED</td><td>supported</td><td>{@link #onPointerPressed(PointerEvent)}</td><td>Allways called</td></tr> * <tr><td>POINTER_DRAGGED</td><td>supported</td><td>{@link #onPointerDragged(PointerEvent)}</td><td>Called only when the scale and move gestures are not in progress</td></tr> * <tr><td>POINTER_RELEASED</td><td>supported</td><td>{@link #onPointerReleased(PointerEvent)}</td><td>Called only when the scale and move gestures are not in progress</td></tr> * <tr><td>POINTER_MOVED</td><td>not supported</td><td></td><td>Pointer move on a touch screen cannot be detected?</td></tr> * <tr><td>POINTER_CLICK</td><td>not supported</td><td></td><td>See {@link #onClick(PointerEvent)}</td></tr> * <tr><td>POINTER_LONG_CLICK</td><td>not supported</td><td></td><td>See {@link #onLongClick(PointerEvent)}</td></tr> * </tbody> * </table> * <p> * The function {@link #onDrawView(Graphics2D, float, CenteringTransform)}} may * use an instance of the graphical context {@link ZoomableGraphics2D} to draw * the elements according to the zooming attributes. * * @author $Author: sgalland$ * @version $Name$ $Revision$ $Date$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @see ZoomableGraphics2D * @deprecated see JavaFX API */ @Deprecated public abstract class ZoomableView extends JPanel implements ZoomableContext, Printable { private static final long serialVersionUID = -5703191414584605699L; /** Size of the border that is automatically added * when {@link #repaint(float, float, float, float)} is invoked. */ public static final int REPAINT_BORDER = 5; /** Indicates if the anti-aliasing flag is set or not. */ private boolean antialiasing = false; /** Current position of the workspace. * This position permits to scroll the view. */ private float focusX = 0f; /** Current position of the workspace. * This position permits to scroll the view. */ private float focusY = 0f; /** Current scaling factor. */ float scaleFactor = 1.f; /** Minimal scaling factor. */ private float minScaleFactor = 0.0001f; /** Maximal scaling factor. */ private float maxScaleFactor = 1000.f; /** Zooming sensibility. */ private float zoomingSensitivity = 1.5f; /** Invert the scrolling direction. */ private boolean isInvertScrollingDirection = false; /** Indicates if the zooming area must be re-centred each time * the mouse wheel was moved. */ private boolean dynamicCenteringWhenWheelMoved = false; /** Transformation to center the view used when rendering the view. */ final CenteringTransform centeringTransform = new CenteringTransform(); /** Indicates if the X axis is inverted or not. */ private boolean isXAxisInverted = false; /** Indicates if the Y axis is inverted or not. */ private boolean isYAxisInverted = false; /** Scrolling method. */ private ScrollingMethod scrollingMethod = ScrollingMethod.MIDDLE_BUTTON; /** Participates to determine if the adviced level of details. */ private Graphics2DLOD lod = Graphics2DLOD.NORMAL_LEVEL_OF_DETAIL; /** Indicates if the zoom factor could be interactively change * with the mouse wheel. */ private boolean isWheelSupport = true; /** Wrapper to the document. */ final DocumentWrapper documentWrapper; /** Internal viewport when scrollbars are activated. */ private final Viewport viewport; /** Horizontal scroll bar. */ final JScrollBar hscroll; /** Vertical scroll bar. */ final JScrollBar vscroll; /** Boolean flag that permits to enable/disable scrollbar updating. */ final AtomicBoolean isScrollbarEnabled = new AtomicBoolean(); /** Create a zoomable view without scrollbars. */ public ZoomableView() { this(null); } /** * @param documentWrapper is the wrapping object to use to obtain * information and notifications about the displayed document. * This wrapper is used to compute the scrollbar's values and * to fit the view. If it is <code>null</code>, the view will * not contain scrollbar. */ public ZoomableView(DocumentWrapper documentWrapper) { super(); this.documentWrapper = documentWrapper; SwingEventHandler handler = new SwingEventHandler(); if (this.documentWrapper!=null) { this.isScrollbarEnabled.set(true); this.viewport = new Viewport(); this.hscroll = new JScrollBar(Adjustable.HORIZONTAL); this.hscroll.setFocusable(false); this.hscroll.setRequestFocusEnabled(false); this.hscroll.setVisible(true); this.vscroll = new JScrollBar(Adjustable.VERTICAL); this.vscroll.setFocusable(false); this.vscroll.setRequestFocusEnabled(false); this.vscroll.setVisible(true); JPanel southPane = new JPanel(); southPane.setLayout(new BoxLayout(southPane, BoxLayout.X_AXIS)); JPanel corner = new JPanel(); Dimension2D d1 = this.hscroll.getPreferredSize(); Dimension2D d2 = this.vscroll.getPreferredSize(); Dimension borderSize = new Dimension((int)d2.getWidth(), (int)d1.getHeight()); corner.setMinimumSize(borderSize); corner.setMaximumSize(borderSize); corner.setPreferredSize(borderSize); setLayout(new BorderLayout()); ComponentOrientation co = getComponentOrientation(); if (co.isLeftToRight()) { add(BorderLayout.EAST, this.vscroll); southPane.add(this.hscroll); southPane.add(corner); } else { add(BorderLayout.WEST, this.vscroll); southPane.add(corner); southPane.add(this.hscroll); } add(BorderLayout.SOUTH, southPane); add(BorderLayout.CENTER, this.viewport); updateScrollbars(); this.vscroll.addAdjustmentListener(handler); this.hscroll.addAdjustmentListener(handler); this.documentWrapper.addChangeListener(handler); } else { this.isScrollbarEnabled.set(false); this.viewport = null; this.hscroll = null; this.vscroll = null; } // Events JPanel viewport = getViewport(); viewport.addComponentListener(handler); viewport.addMouseListener(handler); viewport.addMouseMotionListener(handler); viewport.addMouseWheelListener(handler); viewport.addKeyListener(handler); } /** Invoked to update the scrollbars and the graphical context * according to the current scrollbar context. */ void updateScrollbars() { boolean oldV = this.isScrollbarEnabled.getAndSet(false); try { if (oldV) { assert(this.documentWrapper!=null); Rectangle2f documentBounds = this.documentWrapper.getDocumentBounds(); if (documentBounds==null) { this.hscroll.setEnabled(false); this.vscroll.setEnabled(false); } else { updateScrollBar(this.hscroll, (int)logical2pixel_x(documentBounds.getMinX()), (int)Math.ceil(logical2pixel_x(documentBounds.getMaxX())), getViewport().getWidth()); updateScrollBar(this.vscroll, (int)logical2pixel_y(documentBounds.getMinY()), (int)Math.ceil(logical2pixel_y(documentBounds.getMaxY())), getViewport().getHeight()); } } } finally { this.isScrollbarEnabled.set(oldV); } } private static void updateScrollBar(JScrollBar bar, int minDocument, int maxDocument, int maxViewport) { int minBar = Math.min(minDocument, 0); int maxBar = Math.max(maxDocument, maxViewport); if (minBar>=0 && maxBar<=maxViewport) { bar.setEnabled(false); } else { bar.setEnabled(true); bar.setValues(0, maxViewport, minBar, maxBar); } } /** Replies the viewport. The viewport is the internal * panel if there are scrollbars, or this ZoomableView * if there is no scrollbar. * * @return the internal viewport or this object. */ public JPanel getViewport() { if (this.viewport==null) return this; return this.viewport; } /** Replies if this panel must be drawn with anti-aliased algorithm. * * @return <code>true</code> if the anti-aliased flag was set, * otherwhise <code>false</code> */ public boolean isAntiAliased() { return this.antialiasing; } /** Set if this panel must be drawn with anti-aliased algorithm. * * @param antialiasing must be <code>true</code> if the anti-aliased flag must be set, * otherwhise <code>false</code> */ public void setAntiAliased(boolean antialiasing) { this.antialiasing = antialiasing; } /** Replies the level of details to be used by this * panel when rendering. * The level of details is changed according * to the internal events. * * @return the level of detail */ protected Graphics2DLOD getLOD() { return this.lod; } /** Change the level of details. * * @param lod * @return the old value. */ Graphics2DLOD setLOD(Graphics2DLOD lod) { Graphics2DLOD old = this.lod; this.lod = lod; return old; } /** Replies if this panel allow the user to change the zoom * whith the mouse wheel. * * @return <code>true</code> if the mouse wheel actions are allowed, * otherwise <code>false</code> */ public boolean isMouseWheelAllowed() { return this.isWheelSupport; } /** Sets if this panel allow the user to change the zoom * whith the mouse wheel. * * @param enable must be <code>true</code> if the mouse wheel actions are allowed, * otherwise <code>false</code> */ public void setMouseWheelEnable(boolean enable) { this.isWheelSupport = enable; } /** Replies if this panel recomputes the zooming target point * each time the mouse wheel was move. * * @return <code>true</code> if the target point is set according to the * current mouse position when wheel is used, otherwise <code>false</code> */ public boolean isFocusChangedOnMouseWheel() { return this.dynamicCenteringWhenWheelMoved; } /** Sets if this panel recomputes the zooming target point * each time the mouse wheel was move. * * @param enable must be <code>true</code> if the target point is set according to the * current mouse position when wheel is used, otherwise <code>false</code> */ public void setFocusChangedOnMouseWheel(boolean enable) { this.dynamicCenteringWhenWheelMoved = enable; } /** Replies the scrolling method. * * @return the scrolling method. * @since 4.1 */ public ScrollingMethod getScrollingMethod() { return this.scrollingMethod; } /** Set the scrolling method. * * @param method is the scrolling method. * @since 4.1 */ public void setScrollingMethod(ScrollingMethod method) { this.scrollingMethod = (method==null) ? ScrollingMethod.MIDDLE_BUTTON : method; } @Override public final boolean isXAxisInverted() { return this.isXAxisInverted; } @Override public final boolean isYAxisInverted() { return this.isYAxisInverted; } /** Invert or not the X axis. * <p> * If the X axis is inverted, the positives are to the left; * otherwise they are to the right (default UI standard). * * @param invert */ public final void setXAxisInverted(boolean invert) { if (invert!=this.isXAxisInverted) { this.isXAxisInverted = invert; onUpdateViewParameters(); repaint(); } } /** Invert or not the Y axis. * <p> * If the Y axis is inverted, the positives are to the left; * otherwise they are to the right (default UI standard). * * @param invert */ public final void setYAxisInverted(boolean invert) { if (invert!=this.isYAxisInverted) { this.isYAxisInverted = invert; onUpdateViewParameters(); repaint(); } } @Override public void repaint() { if (this.viewport!=null) this.viewport.repaint(); super.repaint(); } /** Repaint this view wherever this function is invoked. * <p> * This function gets values in the logical coordinate space, * not in the screen coordinate space. To repaint with screen * coordinates, see {@link #repaint(int, int, int, int)}. * <p> * This function the given area extended with a border of 5 pixels. * * @param x is the position in the logical space (not the screen space). * @param y is the position in the logical space (not the screen space). * @param width is the width in the logical space (not the screen space). * @param height is the height in the logical space (not the screen space). * @see #getIgnoreRepaint() * @see #repaint(int, int, int, int) * @see #repaint(Rectangle2f) * @see #REPAINT_BORDER */ public final void repaint(float x, float y, float width, float height) { int l = (int)logical2pixel_x(x)-REPAINT_BORDER; int t = (int)logical2pixel_y(y)-REPAINT_BORDER; int w = (int)logical2pixel_size(width)+REPAINT_BORDER*2; int h = (int)logical2pixel_size(height)+REPAINT_BORDER*2; if (this.viewport==null) repaint(l, t, w, h); else this.viewport.repaint(l, t, w, h); } /** Repaint this view wherever this function is invoked. * <p> * This function gets values in the logical coordinate space, * not in the screen coordinate space. To repaint with screen * coordinates, see {@link #repaint(Rectangle)}. * * @param r is the rectangle to repaint (in logical coordinate space). * @see #repaint(Rectangle) * @see #repaint(float, float, float, float) */ public final void repaint(Rectangle2f r) { if (r!=null) { repaint(r.getMinX(), r.getMinY(), r.getWidth(), r.getHeight()); } } /** Repaint this view wherever this function is invoked. * <p> * This function gets values in the logical coordinate space, * not in the screen coordinate space. To repaint with screen * coordinates, see {@link #repaint(Rectangle)}. * * @param r is the rectangle to repaint (in logical coordinate space). * @see #repaint(Rectangle) * @see #repaint(float, float, float, float) */ public final void repaint(Shape2f r) { if (r!=null) { Rectangle2f bounds = r.toBoundingBox(); repaint(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); } } /** Replies if the direction of moving is inverted. * * @return <code>true</code> if the direction of moving * is inverted; otherwise <code>false</code>. */ public final boolean isMoveDirectionInverted() { return this.isInvertScrollingDirection; } /** Set if the direction of moving is inverted. * * @param invert is <code>true</code> if the direction of moving * is inverted; otherwise <code>false</code>. */ public final void setMoveDirectionInverted(boolean invert) { this.isInvertScrollingDirection = invert; } /** {@inheritDoc} */ @Override public final float logical2pixel_size(float l) { /*float s = l * this.scaleFactor; if ((l!=0)&&(s==0f)) s = 1f; return s;*/ return ZoomableContextUtil.logical2pixel_size(l, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float logical2pixel_x(float l) { /*float dx = l - this.focusX; dx *= getScalingFactor(); return getViewportCenterX() + dx;*/ return ZoomableContextUtil.logical2pixel_x(l, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float logical2pixel_y(float l) { /*float dy = l - this.focusY; dy *= getScalingFactor(); return getViewportCenterY() + dy;*/ return ZoomableContextUtil.logical2pixel_y(l, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float pixel2logical_size(float l) { //return l / this.scaleFactor; return ZoomableContextUtil.pixel2logical_size(l, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float pixel2logical_x(float l) { /*float dx = l - getViewportCenterX(); dx /= getScalingFactor(); return this.focusX + dx;*/ return ZoomableContextUtil.pixel2logical_x(l, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float pixel2logical_y(float l) { /*float dy = l - getViewportCenterY(); dy /= getScalingFactor(); return this.focusY + dy;*/ return ZoomableContextUtil.pixel2logical_y(l, this.centeringTransform, this.scaleFactor); } @Override public PathIterator2f logical2pixel(PathIterator2f p) { return ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scaleFactor); } @Override public PathIterator2f pixel2logical(PathIterator2f p) { return ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Segment2f s) { ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Segment2f s) { ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(RoundRectangle2f r) { ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(RoundRectangle2f r) { ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Point2f p) { ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Point2f p) { ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Ellipse2f e) { ZoomableContextUtil.logical2pixel(e, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Ellipse2f e) { ZoomableContextUtil.pixel2logical(e, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Circle2f r) { ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Circle2f r) { ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Rectangle2f r) { ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Rectangle2f r) { ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor); } @Override public Shape2f logical2pixel(Shape2f s) { return ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scaleFactor); } @Override public Shape2f pixel2logical(Shape2f s) { return ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float getScalingSensitivity() { return this.zoomingSensitivity; } /** Replies the sensivility of the {@code zoomIn()} * and {@code zoomOut()} actions. * * @param sensivility */ public final void setScalingSensitivity(float sensivility) { this.zoomingSensitivity = Math.max(sensivility, Float.MIN_NORMAL); } /** Replies the X coordinate of the center of the viewport (in screen coordinate). * * @return the center of the viewport. */ public final float getViewportCenterX() { return getViewport().getWidth()/2f; } /** Replies the Y coordinate of the center of the viewport (in screen coordinate). * * @return the center of the viewport. */ public final float getViewportCenterY() { return getViewport().getHeight()/2f; } /** {@inheritDoc} */ @Override public final float getFocusX() { return pixel2logical_x(getViewportCenterX()); } /** {@inheritDoc} */ @Override public final float getFocusY() { return pixel2logical_y(getViewportCenterY()); } /** {@inheritDoc} */ @Override public final float getScalingFactor() { return this.scaleFactor; } /** Set the scaling factor. * * @param factor is the scaling factor. * @return <code>true</code> if the scaling factor has changed; * otherwise <code>false</code>. */ public final boolean setScalingFactor(float factor) { if (setScalingFactorAndFocus(Float.NaN, Float.NaN, factor)) { repaint(); return true; } return false; } /** Change the scaling factor to have the specified * ratio between 1 pixel and 1 unit in the document. * <p> * Each unit from the displayed document will * have the same graphical size as the amount of * pixels specified by the <var>ratio</var>. * * @param ratio */ public void setScalingFactorForPixelRatio(float ratio) { float onePixel = pixel2logical_size(1); float factor = onePixel * ratio; setScalingFactor(factor); } /** Set the scaling factor. This function does not repaint. * * @param scalingX is the coordinate of the point (on the screen) where the focus occurs. * @param scalingY is the coordinate of the point (on the screen) where the focus occurs. * @param factor is the scaling factor. * @return <code>true</code> if the scaling factor or the focus point has changed; * otherwise <code>false</code>. */ protected final boolean setScalingFactorAndFocus(float scalingX, float scalingY, float factor) { // Normalize the scaling factor float normalizedFactor = factor; if (normalizedFactor<this.minScaleFactor) normalizedFactor = this.minScaleFactor; if (normalizedFactor>this.maxScaleFactor) normalizedFactor = this.maxScaleFactor; // Determine the new position of the focus. // The new position of the focus depends on the current position, // the new scaling factor and where the scaling occured. if (!Float.isNaN(scalingX) && !Float.isNaN(scalingY)) { // Get screen coordinates float screenCenterX = getViewportCenterX(); float screenCenterY = getViewportCenterY(); float vectorToScreenCenterX = screenCenterX - scalingX; float vectorToScreenCenterY = screenCenterY - scalingY; // Get logical coordinates float sX = pixel2logical_x(scalingX); float sY = pixel2logical_y(scalingY); float newX = sX + vectorToScreenCenterX / normalizedFactor; float newY = sY + vectorToScreenCenterY / normalizedFactor; if (normalizedFactor!=this.scaleFactor || newX!=this.focusX || newY!=this.focusY) { this.scaleFactor = normalizedFactor; this.focusX = newX; this.focusY = newY; onUpdateViewParameters(); return true; } } else if (normalizedFactor!=this.scaleFactor) { this.scaleFactor = normalizedFactor; onUpdateViewParameters(); return true; } return false; } /** Zoom the view in. */ public final void zoomIn() { if (onScale( getViewportCenterX(), getViewportCenterY(), getScalingSensitivity())) { repaint(); } } /** Zoom the view out. */ public final void zoomOut() { if (onScale( getViewportCenterX(), getViewportCenterY(), 1f/getScalingSensitivity())) { repaint(); } } /** {@inheritDoc} */ @Override public final float getMaxScalingFactor() { return this.maxScaleFactor; } /** Set the maximal scaling factor allowing in the view * * @param factor is the maximal scaling factor. */ public final void setMaxScalingFactor(float factor) { if (factor>0f && factor!=this.maxScaleFactor) { this.maxScaleFactor = factor; if (this.minScaleFactor>this.maxScaleFactor) this.minScaleFactor = this.maxScaleFactor; if (this.scaleFactor>this.maxScaleFactor) this.scaleFactor = this.maxScaleFactor; repaint(); } } /** {@inheritDoc} */ @Override public final float getMinScalingFactor() { return this.minScaleFactor; } /** Set the minimal scaling factor allowing in the view * * @param factor is the minimal scaling factor. */ public final void setMinScalingFactor(float factor) { if (factor>0f && factor!=this.minScaleFactor) { this.minScaleFactor = factor; if (this.maxScaleFactor<this.minScaleFactor) this.maxScaleFactor = this.minScaleFactor; if (this.scaleFactor<this.minScaleFactor) this.scaleFactor = this.minScaleFactor; repaint(); } } /** Set the position of the focus point. * * @param x * @param y */ public final void setFocusPoint(float x, float y) { if (this.focusX!=x || this.focusY!=y) { this.focusX = x; this.focusY = y; onUpdateViewParameters(); repaint(); } } /** Translate the position of the focus point. * * @param dx * @param dy */ public final void translateFocusPoint(float dx, float dy) { if (dx!=0f || dy!=0f) { this.focusX += dx; this.focusY += dy; onUpdateViewParameters(); repaint(); } } /** Update any viewing parameter according to the * current value of the focus point and the scaling factor. * <p> * This function is invoked when the coordinates of the * focus point or the scaling factor has been changed to * ensure that all the drawing attributes are properly set. */ protected void onUpdateViewParameters() { /*float sf = getScalingFactor(); float w = getMeasuredWidth() / sf; float h = getMeasuredHeight() / sf; this.translateToCenterX = -(getFocusX() - w/2f); this.translateToCenterY = -(getFocusY() - h/2f);*/ float t; t = pixel2logical_size(getViewportCenterX()); if (isXAxisInverted()) { this.centeringTransform.setCenteringX( true, -1, t + this.focusX); } else { this.centeringTransform.setCenteringX( false, 1, t - this.focusX); } t = pixel2logical_size(getViewportCenterY()); if (isYAxisInverted()) { this.centeringTransform.setCenteringY( true, -1, t + this.focusY); } else { this.centeringTransform.setCenteringY( false, 1, t - this.focusY); } } /** Replies the preferred position of the focus point. * * @return the preferred position of the focus point. */ protected abstract float getPreferredFocusX(); /** Replies the preferred position of the focus point. * * @return the preferred position of the focus point. */ protected abstract float getPreferredFocusY(); /** Reset the view to the default configuration: scaling factor to 1. * * @return <code>true</code> if the view has changed; <code>false</code> otherwise. */ public final boolean resetView() { float px = getPreferredFocusX(); float py = getPreferredFocusY(); if (this.focusX!=px || this.focusY!=py || getScalingFactor()!=1f) { this.focusX = px; this.focusY = py; if (!setScalingFactor(1f)) { onUpdateViewParameters(); repaint(); // Force to refresh the UI } return true; } return false; } /** Replies the scaling factor that may be used * to fit the content of the document to the * drawing area. * <p> * If there is no wrapper given to {@link #ZoomableView(DocumentWrapper)}, * this function replies <code>1</code>. * * @return the scaling factor that permits to fit the * document. */ public float getScalingFactorToFit() { if (this.documentWrapper!=null) { Rectangle2f documentBounds = this.documentWrapper.getDocumentBounds(); if (documentBounds!=null) { JPanel viewport = getViewport(); float drawingAreaSize, documentSize; // horizontal fitting drawingAreaSize = viewport.getWidth(); documentSize = documentBounds.getWidth(); float horizontalFactor = ZoomableContextUtil.determineFactor( documentSize, drawingAreaSize); // vertical fitting drawingAreaSize = viewport.getHeight(); documentSize = documentBounds.getHeight(); float verticalFactor = ZoomableContextUtil.determineFactor( documentSize, drawingAreaSize); return Math.min(horizontalFactor, verticalFactor); } } return 1f; } /** Reset the view so that the document is fitting the * drawing area. * <p> * If there is no wrapper given to {@link #ZoomableView(DocumentWrapper)}, * this function does the same as {@link #resetView()}. * * @return <code>true</code> if the view has changed; <code>false</code> otherwise. */ public final boolean fitView() { float px = getPreferredFocusX(); float py = getPreferredFocusY(); float fitFactor = getScalingFactorToFit(); if (this.focusX!=px || this.focusY!=py || getScalingFactor()!=fitFactor) { this.focusX = px; this.focusY = py; if (!setScalingFactor(fitFactor)) { onUpdateViewParameters(); repaint(); // Force to refresh the UI } return true; } return false; } /** * {@inheritDoc} */ @Override public final int print(Graphics g, PageFormat pageFormat, int pageIndex) throws PrinterException { int result; Graphics2DLOD lod = setLOD(Graphics2DLOD.HIGH_LEVEL_OF_DETAIL); try { if (pageIndex==0) { Graphics2D g2d = (Graphics2D)g; // Shift Graphic to line up with beginning of print-imageable region g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY()); // Be sure that the document feat to the print-imageable region float width = getViewport().getWidth(); float height = getViewport().getHeight(); float pageWidth = (float)pageFormat.getImageableWidth(); float pageHeight = (float)pageFormat.getImageableHeight(); float scale = ((pageWidth / width)>(pageHeight / height)) ? (pageHeight / height) : (pageWidth / width); g2d.setColor(Color.BLACK); g2d.drawRect(0,0,(int)pageWidth,(int)pageHeight); g2d.scale(scale, scale); // Print the document print(g2d, new Rectangle2D.Float(0, 0, width, height)); result = PAGE_EXISTS; } else { result = NO_SUCH_PAGE; } } finally { setLOD(lod); } return result; } /** Print the specified part of this panel into * the specified graphical context. * <p> * The given print area is the part of the panel * to replies inside an image. It is expressed in pixels. * <p> * This function assumes that the upper-left corner * of the document has the coordinates (0,0) and the * specified printing area is relative to this origin. * * @param g is the graphical context inside which to print. * @param print_area is the window coordinates to snap. */ protected final void print(Graphics2D g, Rectangle2D print_area) { float width = getViewport().getWidth(); float height = getViewport().getHeight(); // Be sure that the printing area is enclosed inside the document bounds. Rectangle2D r = new Rectangle2D.Double(0,0,width,height).createIntersection(print_area); // Be sure that the new origin (0,0) corresponds // to upper-left corner of the print area g.translate(-r.getX(),-r.getY()); print(g); } /** * {@inheritDoc} */ @Override public final void print(Graphics g) { Graphics2DLOD lod = setLOD(Graphics2DLOD.HIGH_LEVEL_OF_DETAIL); try { super.print(g); } finally { setLOD(lod); } } /** * {@inheritDoc} */ @Override public final void printAll(Graphics g) { Graphics2DLOD lod = setLOD(Graphics2DLOD.HIGH_LEVEL_OF_DETAIL); try { super.printAll(g); } finally { setLOD(lod); } } @Override public final void paint(Graphics g) { super.paint(g); if (this.viewport==null) onDrawView((Graphics2D)g, this.scaleFactor, this.centeringTransform); } /** Invoked to paint the view after it is translated and scaled. * * @param canvas is the canvas in which the view must be painted. * @param scaleFactor is the scaling factor to use for drawing. * @param centeringTransform is the transform to use to put the draws at the center of the view. */ protected abstract void onDrawView(Graphics2D canvas, float scaleFactor, CenteringTransform centeringTransform); /** * Invoked when a key has been typed. * See the class description for {@link KeyEvent} for a definition of * a key typed event. * <p> * The KEY_TYPED event may be unavailable on several components. * You must read the document of the component on which the action management * is applied to be sure that the KEY_TYPED event is handled. * * @param e */ protected void onKeyTyped(KeyEvent e) { // } /** * Invoked when a key has been pressed. * See the class description for {@link KeyEvent} for a definition of * a key pressed event. * <p> * The KEY_PRESSED event may be unavailable on several components. * You must read the document of the component on which the action management * is applied to be sure that the KEY_PRESSED event is handled. * * @param e */ protected void onKeyPressed(KeyEvent e) { // } /** * Invoked when a key has been released. * See the class description for {@link KeyEvent} for a definition of * a key released event. * <p> * The KEY_RELEASED event may be unavailable on several components. * You must read the document of the component on which the action management * is applied to be sure that the KEY_RELEASED event is handled. * * @param e */ protected void onKeyReleased(KeyEvent e) { // } /** Invoked when the a touch-down event is detected. * * @param e */ protected void onPointerPressed(PointerEvent e) { // } /** Invoked when the a touch-up event is detected. * * @param e */ protected void onPointerReleased(PointerEvent e) { // } /** Invoked when the pointer is moved with a button down. * * @param e */ protected void onPointerDragged(PointerEvent e) { // } /** Invoked when the pointer is moved without a button down. * * @param e */ protected void onPointerMoved(PointerEvent e) { // } /** Invoked when a long-click is detected. * * @param e */ protected void onLongClick(PointerEvent e) { // } /** Invoked when a short-click is detected. * * @param e */ protected void onClick(PointerEvent e) { // } /** * Invoked when the view must be scaled. * <p> * One of the border effect if this function replies <code>true</code> * is that the view will be repaint. * <p> * The default implementation of this function invokes * {@link #setScalingFactorAndFocus(float, float, float)}. * * @param focusX is the position of the focal point on the screen. * @param focusY is the position of the focal point on the screen. * @param requestedScaleFactor is the new scale factor. * @return Whether or not the detector should consider this event as handled. * If an event was not handled, the detector will continue to accumulate movement * until an event is handled. This can be useful if an application, for example, * only wants to update scaling factors if the change is greater than 0.01. * @see #setScalingFactorAndFocus(float, float, float) */ protected boolean onScale(float focusX, float focusY, float requestedScaleFactor) { setScalingFactorAndFocus(focusX, focusY, this.scaleFactor * requestedScaleFactor); return true; } /** * @author $Author: sgalland$ * @version $Name$ $Revision$ $Date$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ private class Viewport extends JPanel { private static final long serialVersionUID = -4516500471311375616L; /** */ public Viewport() { // } @Override public void paint(Graphics g) { super.paint(g); onDrawView((Graphics2D)g, ZoomableView.this.scaleFactor, ZoomableView.this.centeringTransform); } } /** * @author $Author: sgalland$ * @version $Name$ $Revision$ $Date$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ private class SwingEventHandler implements ComponentListener, MouseInputListener, MouseWheelListener, ScrollingMethodListener, AdjustmentListener, ChangeListener, KeyListener { /** Save the number of wheel clicks between two calls to * {@link #mouseWheelMoved(MouseWheelEvent)}. */ private int bufferedWheelClicks = 0; /** Asynchronous process that is waiting before refreshing * the panel with full details */ private volatile ScrollWaiter scrollWaiter = null; /** Indicates if the mouse scrolling feature is under progress. */ private boolean mouseScrollingUnderProgress = false; /** Current location of the mouse on the panel. */ private int mouseX = -1; /** Current location of the mouse on the panel. */ private int mouseY = -1; /** Save the LOD for beeing restored after scrolling. */ private Graphics2DLOD previousLOD = null; public SwingEventHandler() { // } @Override public void componentResized(ComponentEvent e) { onUpdateViewParameters(); } @Override public void componentMoved(ComponentEvent e) { // } @Override public void componentShown(ComponentEvent e) { onUpdateViewParameters(); } @Override public void componentHidden(ComponentEvent e) { // } @Override public void mouseClicked(MouseEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); boolean isLongClick = (e.getClickCount()>1); PointerEventSwing evt = new PointerEventSwing(e); if (isLongClick) { onLongClick(evt); } else { onClick(evt); } } @Override public void mousePressed(MouseEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); getScrollingMethod().tryScroll(e, this); if (!e.isConsumed()) onPointerPressed(new PointerEventSwing(e)); } @Override public void mouseReleased(MouseEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); stopScrolling(e); if (!e.isConsumed()) onPointerReleased(new PointerEventSwing(e)); } @Override public void mouseEntered(MouseEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); } @Override public void mouseExited(MouseEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); } @Override public void mouseDragged(MouseEvent e) { // // MOVE THE VIEW ACCORDING TO THE SCROLLING FEATURE // if (!this.mouseScrollingUnderProgress && this.scrollWaiter!=null) { this.scrollWaiter = null; startScrolling(e, 0); } if (this.mouseScrollingUnderProgress) { Rectangle2f document_rect = ZoomableView.this.documentWrapper.getDocumentBounds(); if (document_rect!=null) { float dx, dy; if (isMoveDirectionInverted()) { dx = pixel2logical_size(this.mouseX - e.getX()); dy = pixel2logical_size(this.mouseY - e.getY()); } else { dx = pixel2logical_size(e.getX() - this.mouseX); dy = pixel2logical_size(e.getY() - this.mouseY); } if ((dx!=0)||(dy!=0)) { float fx = getFocusX() - dx; float fy = getFocusY() - dy; setFocusPoint(fx, fy); } } e.consume(); } this.mouseX = e.getX(); this.mouseY = e.getY(); if (!e.isConsumed()) { onPointerDragged(new PointerEventSwing(e)); } } @Override public void mouseMoved(MouseEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); onPointerMoved(new PointerEventSwing(e)); } @Override public void mouseWheelMoved(MouseWheelEvent e) { this.mouseX = e.getX(); this.mouseY = e.getY(); if (isMouseWheelAllowed()) { int clicks = e.getWheelRotation() + this.bufferedWheelClicks; float fx, fy; if (e.isControlDown() || isFocusChangedOnMouseWheel()) { fx = e.getX(); fy = e.getY(); } else { fx = getViewportCenterX(); fy = getViewportCenterY(); } float scale = getScalingSensitivity(); if (clicks>=0) { scale = 1f / scale; } if (onScale(fx, fy, Math.abs(clicks) * scale)) { this.bufferedWheelClicks = 0; repaint(); } else { this.bufferedWheelClicks = clicks; } e.consume(); } } @Override public void startScrolling(MouseEvent event, int delay) { if (delay<=0) { this.mouseScrollingUnderProgress = true; this.previousLOD = setLOD(Graphics2DLOD.LOW_LEVEL_OF_DETAIL); setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); event.consume(); this.mouseX = event.getX(); this.mouseY = event.getY(); } else if (this.scrollWaiter==null) { this.scrollWaiter = new ScrollWaiter(event, System.currentTimeMillis()+delay); SwingUtilities.invokeLater(this.scrollWaiter); } } @Override public void stopScrolling(MouseEvent event) { this.scrollWaiter = null; if (this.mouseScrollingUnderProgress) { this.mouseScrollingUnderProgress = false; setLOD(this.previousLOD); setCursor(Cursor.getDefaultCursor()); event.consume(); repaint(); this.mouseX = event.getX(); this.mouseY = event.getY(); } } /** This class handles the events for a {@link ZoomablePanel}. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ private class ScrollWaiter implements Runnable { private final long timeout; private final MouseEvent event; /** * @param event * @param timeout */ public ScrollWaiter(MouseEvent event, long timeout) { this.event = event; this.timeout = timeout; } /** * {@inheritDoc} */ @SuppressWarnings("synthetic-access") @Override public void run() { if (System.currentTimeMillis()>=this.timeout) { startScrolling(this.event, 0); } else if (SwingEventHandler.this.scrollWaiter==this) { SwingUtilities.invokeLater(this); } } } // class ScrollWaiter @Override public void adjustmentValueChanged(AdjustmentEvent e) { if (ZoomableView.this.isScrollbarEnabled.get()) { setLOD( e.getValueIsAdjusting() ? Graphics2DLOD.LOW_LEVEL_OF_DETAIL : Graphics2DLOD.NORMAL_LEVEL_OF_DETAIL); float fx = getFocusX(); float fy = getFocusY(); Rectangle2f document_rect = ZoomableView.this.documentWrapper.getDocumentBounds(); if (document_rect!=null) { Rectangle2D viewport = getViewport().getBounds(); if (e.getSource()==ZoomableView.this.vscroll) { // // Move the target point vertically fy = pixel2logical_y(e.getValue() + (float)viewport.getHeight()/2f); } else if (e.getSource()==ZoomableView.this.hscroll) { // // Move the target point horizontally fx = pixel2logical_x(e.getValue() + (float)viewport.getWidth()/2f); } setFocusPoint(fx,fy); } } } @Override public void stateChanged(ChangeEvent e) { updateScrollbars(); } @Override public void keyTyped(java.awt.event.KeyEvent e) { onKeyTyped(new KeyEventSwing(e)); } @Override public void keyPressed(java.awt.event.KeyEvent e) { onKeyPressed(new KeyEventSwing(e)); } @Override public void keyReleased(java.awt.event.KeyEvent e) { onKeyReleased(new KeyEventSwing(e)); } } // class SwingEventHandler }