/* Copyright (c) 2006-2007 Timothy Wall, All Rights Reserved
*
* This library 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 2.1 of the License, or (at your option) any later version.
* <p/>
* This library 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.
*/
package furbelow;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Window;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JRootPane;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
/** Component which scales any given {@link JComponent} into its bounds.
* The visible portion of the {@link JComponent} (as reported by
* {@link JComponent#getVisibleRect}) is drawn as a rectangle in the scaled
* image. Dragging the rectangle will move the visible portion of the
* panned component within its scrolling context.
*/
public class Panner extends JComponent {
public static final int MINIMUM_WIDTH = 64;
public static final int MINIMUM_HEIGHT = 64;
private static final Color VISIBLE_BOUNDS_COLOR =
new Color(128, 128, 128, 64);
private static final Color BORDER_COLOR = Color.black;
private JComponent panned;
private ScaledIcon thumbnail;
private float transparency = 0.9f;
private boolean preserveAspect = true;
private boolean includeBorder = true;
/** Whether to center the thumbnail if the component doesn't match
* the panned component's aspect ratio.
*/
private boolean centered = true;
/** Whether the thumbnail shades to show visible (versus an outline). */
private boolean shadeHidden = true;
/** Whether this thumbnail is attached to its panned component. */
private boolean attached;
/** Listener for notifications when the panned component is scrolled. */
private ComponentListener listener = new PannedListener();
public Panner() {
this(null);
}
public Panner(JComponent reference) {
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
if (panned != null)
setViewportCenter(e.getPoint());
}
});
addMouseMotionListener(new MouseMotionListener() {
// TODO: revert if dragged far enough away?
public void mouseDragged(MouseEvent e) {
if (panned != null)
setViewportCenter(e.getPoint());
}
public void mouseMoved(MouseEvent e) { }
});
setPannedComponent(reference);
}
public void setPreserveAspect(boolean preserve) {
preserveAspect = preserve;
thumbnail.setPreserveAspect(preserveAspect);
}
public void setIncludeBorder(boolean border) {
if (border != includeBorder) {
boolean old = includeBorder;
this.includeBorder = border;
firePropertyChange("includeBorder", old, includeBorder);
}
}
public void setTransparency(float t) {
if (t != transparency) {
float old = this.transparency;
this.transparency = t;
firePropertyChange("transparency", old, transparency);
}
}
/** "Attach" to the panned component at the given location within
* the component. Returns whether the attach was successful.
*/
public boolean attach(int x, int y) {
int layerOffset = 1000;
JRootPane root = panned.getRootPane();
if (root != null) {
JLayeredPane lp = root.getLayeredPane();
Component child = panned;
while (child.getParent() != lp) {
child = child.getParent();
}
Point pt = SwingUtilities.convertPoint(panned, x, y, lp);
Dimension size = getPreferredSize();
// Don't adjust the thumbnail size!
super.setBounds(pt.x, pt.y, size.width, size.height);
int layer = lp.getLayer(child);
// NOTE: JLayeredPane doesn't properly repaint an overlapping
// child when an obscured child calls repaint(), if the two
// are in the same layer.
lp.add(this, new Integer(layer + layerOffset), 0);
panned.repaint();
revalidate();
repaint();
boolean wasAttached = attached;
attached = true;
firePropertyChange("attached", wasAttached, true);
}
return attached;
}
public boolean isAttached() { return attached; }
public void detach() {
if (attached) {
Container parent = getParent();
if (parent != null) {
parent.remove(this);
parent.invalidate();
parent.repaint();
}
attached = false;
firePropertyChange("attached", true, false);
}
}
/** Sets the center point of the current viewport. Coordinates are relative
* to the Panner bounds. The viewport bounds will always be contained
* within the thumbnail image.
* @param where
*/
public void setViewportCenter(Point where) {
Rectangle bounds = getThumbnailBounds();
Rectangle visible = getViewportBounds();
visible.x = where.x - bounds.x - (int)Math.round(visible.width/2.0);
visible.y = where.y - bounds.y - (int)Math.round(visible.height/2.0);
double[] scale = scaleFactor();
Rectangle current = panned.getVisibleRect();
Dimension size = getDrawingSize(panned);
current.x = Math.min(size.width,
Math.max(0, (int)
Math.round(visible.x / scale[0])));
current.y = Math.min(size.height,
Math.max(0, (int)
Math.round(visible.y / scale[1])));
panned.scrollRectToVisible(current);
}
protected Dimension getDrawingSize(JComponent component) {
Dimension size = check(component.getSize());
Insets insets = component.getInsets();
if (insets != null) {
size.width -= insets.left + insets.right;
size.height -= insets.top + insets.bottom;
}
return size;
}
/** Set the thumbnail alignment within the available space. */
public void setCentered(boolean set) {
boolean oldCentered = centered;
centered = set;
repaint();
firePropertyChange("centered", oldCentered, centered);
}
/** @return whether the thumbnail is centered within the available space. */
public boolean isCentered() { return centered; }
/** Set the component being panned. */
public void setPannedComponent(JComponent panned) {
JComponent oldPanned = this.panned;
if (oldPanned != null) {
detach();
oldPanned.removeComponentListener(listener);
}
this.panned = panned;
thumbnail = new ScaledIcon(new ComponentIcon(panned, includeBorder));
thumbnail.setPreserveAspect(preserveAspect);
setThumbnailSize();
panned.addComponentListener(listener);
revalidate();
repaint();
firePropertyChange("panned", oldPanned, panned);
}
protected Dimension check(Dimension size) {
size.width = Math.max(size.width, MINIMUM_WIDTH);
size.height = Math.max(size.height, MINIMUM_HEIGHT);
return size;
}
/** Return the actual thumbnail bounds, accounting for extra space
* required for this component's border and to maintain proper aspect ratio.
*/
public Rectangle getThumbnailBounds() {
Dimension thumb = getThumbnailSize();
int x = 0;
int y = 0;
if (centered) {
x += (getWidth() - thumb.width)/2;
y += (getHeight() - thumb.height)/2;
}
return new Rectangle(x, y, thumb.width, thumb.height);
}
/** Return a rectangle within the current component content bounds
* equivalent to the visible rectangle within the panned component's
* content bounds.
*/
public Rectangle getViewportBounds() {
if (panned == null)
return getVisibleRect();
Rectangle bounds = getThumbnailBounds();
Rectangle rect = panned.getVisibleRect();
double[] scale = scaleFactor();
rect.x = bounds.x + (int)Math.round(rect.x * scale[0]);
rect.y = bounds.y + (int)Math.round(rect.y * scale[1]);
rect.width = Math.min(bounds.width, (int)Math.round(rect.width * scale[0]));
rect.height = Math.min(bounds.height, (int)Math.round(rect.height* scale[1]));
if (rect.x + rect.width > bounds.x + bounds.width) {
rect.x = bounds.x + bounds.width - rect.width;
}
if (rect.y + rect.height > bounds.y + bounds.height) {
rect.y = bounds.y + bounds.height - rect.height;
}
return rect;
}
private Dimension getThumbnailSize() {
return new Dimension(thumbnail.getIconWidth(),
thumbnail.getIconHeight());
}
private double[] scaleFactor() {
if (panned == null)
return new double[] { 1.0, 1.0 };
Dimension full = getDrawingSize(panned);
Rectangle bounds = getThumbnailBounds();
return new double[] {
(double)bounds.width / full.width,
(double)bounds.height / full.height
};
}
/** Returns the preferred size, which will be the set preferred size
* or the current size with an appropriate aspect ratio applied.
* If there is no current panned component, no aspect ratio will be
* applied.
*/
public Dimension getPreferredSize() {
if (isPreferredSizeSet())
return super.getPreferredSize();
Dimension size = check(getThumbnailSize());
Insets insets = getInsets();
if (insets != null) {
size.width += insets.left + insets.right;
size.height += insets.top + insets.bottom;
}
return size;
}
/** Ensure the maximum size always has the correct aspect ratio. */
public Dimension getMaximumSize() {
return isMaximumSizeSet() ? super.getMaximumSize() : getPreferredSize();
}
/** Ensure the minimum size always has the correct aspect ratio. */
public Dimension getMinimumSize() {
return isMinimumSizeSet() ? super.getMinimumSize() : getPreferredSize();
}
public void setBounds(int x, int y, int w, int h) {
super.setBounds(x, y, w, h);
setThumbnailSize();
}
private void setThumbnailSize() {
thumbnail.setPreserveAspect(preserveAspect);
thumbnail.setSize(getDrawingSize(this));
if (thumbnail.getIconWidth() < MINIMUM_WIDTH
|| thumbnail.getIconHeight() < MINIMUM_HEIGHT) {
thumbnail.setPreserveAspect(false);
thumbnail.setSize(getDrawingSize(this));
}
}
/** Paint the panned component in a thumbnail. */
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
Composite oldComposite = g2d.getComposite();
if (attached) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, transparency));
}
try {
super.paint(g);
if (panned == null)
return;
Rectangle bounds = getThumbnailBounds();
Shape oldClip = g2d.getClip();
try {
Area mask = new Area(bounds);
g2d.setClip(mask);
thumbnail.paintIcon(this, g2d, bounds.x, bounds.y);
}
finally {
g2d.setClip(oldClip);
}
Color oldColor = g.getColor();
// Indicate the visible rect
Rectangle visible = getViewportBounds();
g.setColor(VISIBLE_BOUNDS_COLOR);
if (shadeHidden) {
oldClip = g.getClip();
Area area = new Area(bounds);
area.subtract(new Area(visible));
((Graphics2D)g).setClip(area);
g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
((Graphics2D)g).setClip(oldClip);
}
else {
g.drawRect(visible.x, visible.y, visible.width, visible.height);
}
// Always paint a thin border (NOT the same thing as setBorder, since
// the thumbnail outline may be smaller than the actual component
g.setColor(BORDER_COLOR);
g.drawRect(bounds.x, bounds.y, bounds.width-1, bounds.height-1);
g.setColor(oldColor);
}
finally {
g2d.setComposite(oldComposite);
}
}
private final class PannedListener extends ComponentAdapter
implements HierarchyListener, PropertyChangeListener {
public void hierarchyChanged(HierarchyEvent e) {
if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) != 0) {
detach();
}
}
public void propertyChange(PropertyChangeEvent e) {
if (JLayeredPane.LAYER_PROPERTY.equals(e.getPropertyName())) {
detach();
}
}
public void componentResized(ComponentEvent e) {
setThumbnailSize();
revalidate();
repaint();
}
public void componentMoved(ComponentEvent e) {
repaint();
}
}
}