/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* 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 3 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, see http://www.gnu.org/licenses/
*/
package com.bc.ceres.glayer.swing;
import com.bc.ceres.core.Assert;
import com.bc.ceres.grender.AdjustableView;
import com.bc.ceres.grender.Viewport;
import com.bc.ceres.grender.ViewportListener;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.Rectangle2D;
/**
* A <code>ViewPane</code> is an alternative to {@link javax.swing.JScrollPane}
* when you need to scroll an infinite area given in floating-point coordinates.
* <p/>
* In opposite to {@link javax.swing.JScrollPane}, we don't scroll a view given
* by a {@link javax.swing.JComponent} but it's {@link Viewport}. For this reason
* the view component must implement the {@link AdjustableView} interface.
*
* @author Norman Fomferra
* @version $Revision$ $Date$
*/
public class AdjustableViewScrollPane extends JPanel {
private static final long serialVersionUID = -2634482999458990218L;
// Extension of model bounds in view coordinates
private static final int MODEL_BOUNDS_EXTENSION = 10; // pixels
// Maximum scroll bar value
private static final int MAX_SB_VALUE = 10000;
private JScrollBar horizontalScrollBar;
private JScrollBar verticalScrollBar;
private JComponent cornerComponent;
private JComponent viewComponent;
private AdjustableView adjustableView;
private Rectangle2D scrollArea;
private boolean updatingScrollBars;
private boolean scrollBarsUpdated;
private ViewportChangeHandler viewportChangeHandler;
private boolean hsbVisible;
private boolean vsbVisible;
private boolean debug = false;
/**
* Constructs a new view pane with an empty view component.
*/
public AdjustableViewScrollPane() {
this(null);
}
/**
* Constructs a new view pane with the given view viewComponent
*
* @param viewComponent the view viewComponent. If not null, it must implement {@link AdjustableView}.
*/
public AdjustableViewScrollPane(JComponent viewComponent) {
super(null);
Assert.notNull(viewComponent, "viewComponent");
Assert.argument(viewComponent instanceof AdjustableView, "viewComponent");
scrollArea = new Rectangle2D.Double();
viewportChangeHandler = new ViewportChangeHandler();
setViewComponent(viewComponent);
setCornerComponent(createCornerComponent());
final ChangeListener scrollBarCH = new ScrollBarChangeHandler();
horizontalScrollBar = createHorizontalScrollbar();
horizontalScrollBar.getModel().addChangeListener(scrollBarCH);
verticalScrollBar = createVerticalScrollBar();
verticalScrollBar.getModel().addChangeListener(scrollBarCH);
addComponentListener(new ResizeHandler());
}
public AdjustableView getAdjustableView() {
return (AdjustableView) viewComponent;
}
public JComponent getViewComponent() {
return viewComponent;
}
/**
* Constructs a new view pane with the given view which must implement the {@link AdjustableView} interface.
*
* @param viewComponent a view component implement {@link AdjustableView}.
*/
public void setViewComponent(JComponent viewComponent) {
if (this.viewComponent != viewComponent) {
if (this.viewComponent != null) {
this.adjustableView.getViewport().removeListener(viewportChangeHandler);
remove(this.viewComponent);
}
this.viewComponent = viewComponent;
this.adjustableView = null;
if (viewComponent != null) {
this.adjustableView = (AdjustableView) viewComponent;
this.adjustableView.getViewport().addListener(viewportChangeHandler);
add(this.viewComponent);
}
revalidate();
validate();
}
}
public JComponent getCornerComponent() {
return cornerComponent;
}
public void setCornerComponent(JComponent cornerComponent) {
if (this.cornerComponent != cornerComponent) {
this.cornerComponent = cornerComponent;
revalidate();
validate();
}
}
@Override
public void doLayout() {
if (viewComponent == null || !viewComponent.isVisible()) {
return;
}
if (!scrollBarsUpdated) {
updateScrollBars();
updateScrollBarIncrements();
}
final Insets insets = getInsets();
final int width = getWidth() - (insets.left + insets.right);
final int height = getHeight() - (insets.top + insets.bottom);
if (width <= 0 || height <= 0) {
return;
}
// x1 w1 x2 w2
// +-------------------------------+---+ y1
// | | |
// | | v |
// | | s |
// | | b |
// | view | | h1
// | | |
// | | |
// | | |
// | | |
// +-------------------------------+---+ y2
// | hsb | | h2
// +-------------------------------+---+
//
if (hsbVisible && vsbVisible) {
final Dimension hsbSize = horizontalScrollBar.getPreferredSize();
final Dimension vsbSize = verticalScrollBar.getPreferredSize();
final int x1 = insets.left;
final int y1 = insets.top;
final int w2 = vsbSize.width;
final int h2 = hsbSize.height;
final int w1 = width - w2;
final int h1 = height - h2;
final int x2 = x1 + w1;
final int y2 = y1 + h1;
viewComponent.setBounds(x1, y1, w1, h1);
verticalScrollBar.setBounds(x2, y1, w2, h1);
horizontalScrollBar.setBounds(x1, y2, w1, h2);
if (cornerComponent != null) {
cornerComponent.setBounds(x2, y2, w2, h2);
}
} else if (hsbVisible) {
final Dimension hsbSize = horizontalScrollBar.getPreferredSize();
final int x1 = insets.left;
final int y1 = insets.top;
final int w1 = width;
final int h2 = hsbSize.height;
final int h1 = height - h2;
final int y2 = y1 + h1;
viewComponent.setBounds(x1, y1, w1, h1);
horizontalScrollBar.setBounds(x1, y2, w1, h2);
} else if (vsbVisible) {
final Dimension vsbSize = verticalScrollBar.getPreferredSize();
final int x1 = insets.left;
final int y1 = insets.top;
final int w2 = vsbSize.width;
final int w1 = width - w2;
final int h1 = height;
final int x2 = x1 + w1;
viewComponent.setBounds(x1, y1, w1, h1);
verticalScrollBar.setBounds(x2, y1, w2, h1);
} else {
final int x1 = insets.left;
final int y1 = insets.top;
viewComponent.setBounds(x1, y1, width, height);
}
viewComponent.doLayout();
}
@Override
protected void addImpl(Component comp, Object constraints, int index) {
if (comp != horizontalScrollBar &&
comp != verticalScrollBar &&
comp != cornerComponent &&
comp != viewComponent) {
throw new IllegalArgumentException();
}
super.addImpl(comp, constraints, index);
}
/**
* @return <code>new JScrollBar(JScrollBar.HORIZONTAL)</code>
*/
protected JScrollBar createHorizontalScrollbar() {
return new JScrollBar(JScrollBar.HORIZONTAL);
}
/**
* @return <code>new JScrollBar(JScrollBar.VERTICAL)</code>
*/
protected JScrollBar createVerticalScrollBar() {
return new JScrollBar(JScrollBar.VERTICAL);
}
/**
* @return <code>new JPanel()</code>
*/
protected JPanel createCornerComponent() {
return new JPanel();
}
private void updateViewport() {
if (updatingScrollBars || adjustableView == null) {
if (debug) {
System.out.println("AdjustableViewScrollPane.updateViewport: return!");
}
return;
}
final Rectangle va = getViewBounds();
double vx = va.getX();
double vy = va.getY();
final Rectangle2D sa = scrollArea;
if (hsbVisible) {
final int hsbValue = horizontalScrollBar.getValue();
vx = sa.getX() + hsbValue * sa.getWidth() / MAX_SB_VALUE;
}
if (vsbVisible) {
final int vsbValue = verticalScrollBar.getValue();
vy = sa.getY() + vsbValue * sa.getHeight() / MAX_SB_VALUE;
}
if (hsbVisible || vsbVisible) {
if (debug) {
System.out.println("AdjustableViewScrollPane.updateViewport:");
System.out.println(" vx = " + vx);
System.out.println(" vy = " + vy);
System.out.println("");
}
adjustableView.getViewport().moveViewDelta(-vx, -vy);
}
}
private void updateScrollBars() {
if (adjustableView == null) {
if (debug) {
System.out.println("AdjustableViewScrollPane.updateScrollBars: return!");
}
return;
}
// View bounds in view coordinates
final Rectangle2D va = getViewBounds();
if (va.isEmpty()) {
remove(horizontalScrollBar);
remove(verticalScrollBar);
if (cornerComponent != null) {
remove(cornerComponent);
}
return;
}
// Model bounds in view coordinates
final Rectangle2D ma = adjustableView.getViewport().getModelToViewTransform().createTransformedShape(adjustableView.getMaxVisibleModelBounds()).getBounds2D();
// Following code make it easier to scroll out of the model area
ma.add(ma.getX() - MODEL_BOUNDS_EXTENSION, ma.getY() - MODEL_BOUNDS_EXTENSION);
ma.add(ma.getX() + ma.getWidth() + MODEL_BOUNDS_EXTENSION, ma.getY() + ma.getHeight() + MODEL_BOUNDS_EXTENSION);
// Scroll bounds in view coordinates
final Rectangle2D sa = ma.createUnion(va);
// x1,x2,y1,y2(+) no scrollbars x1,x2,y1,y2(-) V+H-scrollbars
// +--------------------------------+ +--------------------------------+
// | va ^ | | ma ^ |
// | | | | | |
// | y1 | | y1 |
// | | | | | |
// | v | | v |
// | +--------------+ | | +--------------+ |
// | | ma | | | | va V |
// |<--x1-->| |<--x2-->| |<--x1-->| V<--x2-->|
// | | | | | | V |
// | +--------------+ | | +HHHHHHHHHHHHHH+ |
// | ^ | | ^ |
// | | | | | |
// | y2 | | y2 |
// | | | | | |
// | v | | v |
// +--------------------------------+ +--------------------------------+
//
// x1,y1,y2(+) x2(-) H-scrollbar x1,y1,y2(+) x2(-) H,V-scrollbars
// +--------------------------------+ +--------------------------------+
// | va ^ | | ma ^ |
// | | | | | |
// | y1 | | y1 |
// | | |<--x2--> | | |<--x2-->
// | v | | v |
// | +-------------------+ | +-------------------+
// | | ma | | | va V
// |<---------x1-------->| | |<---------x1-------->| V
// | | | | | V
// | +-------------------+ | +HHHHHHHHHHHHHHHHHHH+
// | ^ | | ^ |
// | | | | | |
// | y2 | | y2 |
// | | | | | |
// | v | | v |
// +HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH+ +--------------------------------+
//
final double dx1 = ma.getX() - va.getX();
final double dy1 = ma.getY() - va.getY();
final double dx2 = (va.getX() + va.getWidth()) - (ma.getX() + ma.getWidth());
final double dy2 = (va.getY() + va.getHeight()) - (ma.getY() + ma.getHeight());
boolean hsbVisible = dx1 < 0 || dx2 < 0;
boolean vsbVisible = dy1 < 0 || dy2 < 0;
if (this.hsbVisible != hsbVisible || this.vsbVisible != vsbVisible) {
if (this.hsbVisible != hsbVisible) {
if (hsbVisible) {
add(horizontalScrollBar);
} else {
remove(horizontalScrollBar);
}
}
if (this.vsbVisible != vsbVisible) {
if (vsbVisible) {
add(verticalScrollBar);
} else {
remove(verticalScrollBar);
}
}
if (cornerComponent != null) {
if (hsbVisible && vsbVisible) {
add(cornerComponent);
} else {
remove(cornerComponent);
}
}
this.hsbVisible = hsbVisible;
this.vsbVisible = vsbVisible;
}
if (debug) {
System.out.println("AdjustableViewScrollPane.updateScrollBars:");
System.out.println(" hsbVisible = " + vsbVisible);
System.out.println(" hsbVisible = " + hsbVisible);
System.out.println(" va = " + va);
System.out.println(" ma = " + ma);
System.out.println(" sa = " + sa);
System.out.println(" dx1 = " + dx1 + ", dx2 = " + dx2);
System.out.println(" dy1 = " + dy1 + ", dy2 = " + dy2);
System.out.println();
}
scrollArea.setRect(sa);
updatingScrollBars = true;
if (hsbVisible) {
int hsbValue = (int) Math.round(MAX_SB_VALUE * (va.getX() - sa.getX()) / sa.getWidth());
hsbValue = clamp(hsbValue, 0, MAX_SB_VALUE);
int hsbExtend = (int) Math.round(MAX_SB_VALUE * va.getWidth() / sa.getWidth());
hsbExtend = clamp(hsbExtend, 0, MAX_SB_VALUE);
horizontalScrollBar.setValues(hsbValue, hsbExtend, 0, MAX_SB_VALUE);
}
if (vsbVisible) {
int vsbValue = (int) Math.round(MAX_SB_VALUE * (va.getY() - sa.getY()) / sa.getHeight());
vsbValue = clamp(vsbValue, 0, MAX_SB_VALUE);
int vsbExtend = (int) Math.round(MAX_SB_VALUE * va.getHeight() / sa.getHeight());
vsbExtend = clamp(vsbExtend, 0, MAX_SB_VALUE);
verticalScrollBar.setValues(vsbValue, vsbExtend, 0, MAX_SB_VALUE);
}
updatingScrollBars = false;
scrollBarsUpdated = true;
}
private void updateScrollBarIncrements() {
// we could set more reasonable increments at this place, e.g. using the viewport.modelArea property
horizontalScrollBar.setUnitIncrement(Math.max(10, MAX_SB_VALUE / 50));
horizontalScrollBar.setBlockIncrement(Math.max(10, MAX_SB_VALUE / 5));
verticalScrollBar.setUnitIncrement(Math.max(10, MAX_SB_VALUE / 50));
verticalScrollBar.setBlockIncrement(Math.max(10, MAX_SB_VALUE / 5));
}
private Rectangle getViewBounds() {
return new Rectangle(0, 0, viewComponent.getWidth(), viewComponent.getHeight());
}
private static int clamp(int value, int min, int max) {
return (value < min) ? min : (value > max) ? max : value;
}
private class ResizeHandler extends ComponentAdapter {
@Override
public void componentResized(ComponentEvent e) {
updateScrollBars();
}
}
private class ScrollBarChangeHandler implements ChangeListener {
boolean atWork;
@Override
public void stateChanged(ChangeEvent e) {
if (!atWork) {
try {
atWork = true;
updateViewport();
} finally {
atWork = false;
}
}
}
}
private class ViewportChangeHandler implements ViewportListener {
boolean atWork;
@Override
public void handleViewportChanged(Viewport viewport, boolean orientationChanged) {
if (!atWork) {
try {
atWork = true;
updateScrollBars();
updateScrollBarIncrements();
} finally {
atWork = false;
}
}
}
}
}