/* JViewport.java -- Copyright (C) 2002, 2004, 2005 Free Software Foundation, Inc. This file is part of GNU Classpath. GNU Classpath 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 2, or (at your option) any later version. GNU Classpath 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 GNU Classpath; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ package javax.swing; import gnu.classpath.SystemProperties; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.io.Serializable; import javax.accessibility.Accessible; import javax.accessibility.AccessibleContext; import javax.accessibility.AccessibleRole; import javax.swing.border.Border; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.ViewportUI; /** * *
 *                                                     _
 *   +-------------------------------+    ...........Y1 \
 *   |  view                         |                .  \
 *   |  (this component's child)     |                .   > VY
 *   |                               |                .  / = Y2-Y1
 *   |         +------------------------------+  ....Y2_/
 *   |         | viewport            |        |       .
 *   |         | (this component)    |        |       .
 *   |         |                     |        |       .
 *   |         |                     |        |       .
 *   |         |                     |        |       .
 *   |         |                     |        |       .
 *   |         +------------------------------+  ....Y3
 *   |                               |                .
 *   |         .                     |        .       .
 *   |         .                     |        .       .
 *   +---------.---------------------+    ...........Y4
 *   .         .                     .        .
 *   .         .                     .        .
 *   .         .                     .        .
 *   X1.......X2.....................X3.......X4
 *   \____  ___/
 *        \/
 *        VX = X2-X1
 *
* *

A viewport is, like all swing components, located at some position in * the swing component tree; that location is exactly the same as any other * components: the viewport's "bounds".

* *

But in terms of drawing its child, the viewport thinks of itself as * covering a particular position of the view's coordinate space. * For example, the {@link #getViewPosition} method returns * the position (VX,VY) shown above, which is an position in * "view space", even though this is implemented by positioning * the underlying child at position (-VX,-VY)

* */ public class JViewport extends JComponent implements Accessible { /** * Provides accessibility support for JViewport. * * @author Roman Kennke (roman@kennke.org) */ protected class AccessibleJViewport extends AccessibleJComponent { /** * Creates a new instance of AccessibleJViewport. */ protected AccessibleJViewport() { // Nothing to do here. } /** * Returns the accessible role of JViewport, which is * {@link AccessibleRole#VIEWPORT}. * * @return the accessible role of JViewport */ public AccessibleRole getAccessibleRole() { return AccessibleRole.VIEWPORT; } } /** * A {@link java.awt.event.ComponentListener} that listens for * changes of the view's size. This triggers a revalidate() call on the * viewport. */ protected class ViewListener extends ComponentAdapter implements Serializable { private static final long serialVersionUID = -2812489404285958070L; /** * Creates a new instance of ViewListener. */ protected ViewListener() { // Nothing to do here. } /** * Receives notification when a component (in this case: the view * component) changes it's size. This simply triggers a revalidate() on the * viewport. * * @param ev the ComponentEvent describing the change */ public void componentResized(ComponentEvent ev) { // Fire state change, because resizing the view means changing the // extentSize. fireStateChanged(); revalidate(); } } public static final int SIMPLE_SCROLL_MODE = 0; public static final int BLIT_SCROLL_MODE = 1; public static final int BACKINGSTORE_SCROLL_MODE = 2; private static final long serialVersionUID = -6925142919680527970L; /** * The default scrollmode to be used by all JViewports as determined by * the system property gnu.javax.swing.JViewport.scrollMode. */ private static final int defaultScrollMode; protected boolean scrollUnderway; protected boolean isViewSizeSet; /** * This flag indicates whether we use a backing store for drawing. * * @deprecated since JDK 1.3 */ protected boolean backingStore; /** * The backingstore image used for the backingstore and blit scroll methods. */ protected Image backingStoreImage; /** * The position at which the view has been drawn the last time. This is used * to determine the bittable area. */ protected Point lastPaintPosition; ChangeEvent changeEvent = new ChangeEvent(this); int scrollMode; /** * The ViewListener instance. */ ViewListener viewListener; /** * Stores the location from where to blit. This is a cached Point object used * in blitting calculations. */ Point cachedBlitFrom; /** * Stores the location where to blit to. This is a cached Point object used * in blitting calculations. */ Point cachedBlitTo; /** * Stores the width of the blitted area. This is a cached Dimension object * used in blitting calculations. */ Dimension cachedBlitSize; /** * Stores the bounds of the area that needs to be repainted. This is a cached * Rectangle object used in blitting calculations. */ Rectangle cachedBlitPaint; boolean damaged = true; /** * A flag indicating if the size of the viewport has changed since the * last repaint. This is used in double buffered painting to check if we * need a new double buffer, or can reuse the old one. */ boolean sizeChanged = true; /** * Indicates if this JViewport is the paint root or not. If it is not, then * we may not assume that the offscreen buffer still has the right content * because parent components may have cleared the background already. */ private boolean isPaintRoot = false; /** * Initializes the default setting for the scrollMode property. */ static { String scrollModeProp = SystemProperties.getProperty("gnu.swing.scrollmode", "BACKINGSTORE"); if (scrollModeProp.equalsIgnoreCase("simple")) defaultScrollMode = SIMPLE_SCROLL_MODE; else if (scrollModeProp.equalsIgnoreCase("backingstore")) defaultScrollMode = BACKINGSTORE_SCROLL_MODE; else defaultScrollMode = BLIT_SCROLL_MODE; } public JViewport() { setOpaque(true); setScrollMode(defaultScrollMode); updateUI(); setLayout(createLayoutManager()); lastPaintPosition = new Point(); cachedBlitFrom = new Point(); cachedBlitTo = new Point(); cachedBlitSize = new Dimension(); cachedBlitPaint = new Rectangle(); } public Dimension getExtentSize() { return getSize(); } public Dimension toViewCoordinates(Dimension size) { return size; } public Point toViewCoordinates(Point p) { Point pos = getViewPosition(); return new Point(p.x + pos.x, p.y + pos.y); } public void setExtentSize(Dimension newSize) { Dimension oldExtent = getExtentSize(); if (! newSize.equals(oldExtent)) { setSize(newSize); fireStateChanged(); } } /** * Returns the viewSize when set, or the preferred size of the set * Component view. If no viewSize and no Component view is set an * empty Dimension is returned. */ public Dimension getViewSize() { Dimension size; Component view = getView(); if (view != null) { if (isViewSizeSet) size = view.getSize(); else size = view.getPreferredSize(); } else size = new Dimension(0, 0); return size; } public void setViewSize(Dimension newSize) { Component view = getView(); if (view != null) { if (! newSize.equals(view.getSize())) { scrollUnderway = false; view.setSize(newSize); isViewSizeSet = true; fireStateChanged(); } } } /** * Get the viewport's position in view space. Despite confusing name, * this really does return the viewport's (0,0) position in view space, * not the view's position. */ public Point getViewPosition() { Component view = getView(); if (view == null) return new Point(0,0); else { Point p = view.getLocation(); p.x = -p.x; p.y = -p.y; return p; } } public void setViewPosition(Point p) { Component view = getView(); if (view != null && ! p.equals(getViewPosition())) { scrollUnderway = true; view.setLocation(-p.x, -p.y); fireStateChanged(); } } public Rectangle getViewRect() { return new Rectangle(getViewPosition(), getExtentSize()); } /** * @deprecated 1.4 */ public boolean isBackingStoreEnabled() { return scrollMode == BACKINGSTORE_SCROLL_MODE; } /** * @deprecated 1.4 */ public void setBackingStoreEnabled(boolean b) { if (b && scrollMode != BACKINGSTORE_SCROLL_MODE) { scrollMode = BACKINGSTORE_SCROLL_MODE; fireStateChanged(); } } public void setScrollMode(int mode) { scrollMode = mode; fireStateChanged(); } public int getScrollMode() { return scrollMode; } public Component getView() { if (getComponentCount() == 0) return null; return getComponents()[0]; } public void setView(Component v) { Component currView = getView(); if (viewListener != null && currView != null) currView.removeComponentListener(viewListener); if (v != null) { if (viewListener == null) viewListener = createViewListener(); v.addComponentListener(viewListener); add(v); fireStateChanged(); } revalidate(); repaint(); } public void reshape(int x, int y, int w, int h) { if (w != getWidth() || h != getHeight()) sizeChanged = true; super.reshape(x, y, w, h); if (sizeChanged) { damaged = true; fireStateChanged(); } } public final Insets getInsets() { return new Insets(0, 0, 0, 0); } public final Insets getInsets(Insets insets) { if (insets == null) return getInsets(); insets.top = 0; insets.bottom = 0; insets.left = 0; insets.right = 0; return insets; } /** * Overridden to return false, so the JViewport's paint method * gets called instead of directly calling the children. This is necessary * in order to get a useful clipping and translation on the children. * * @return false */ public boolean isOptimizedDrawingEnabled() { return false; } public void paint(Graphics g) { Component view = getView(); if (view == null) return; Rectangle viewBounds = view.getBounds(); Rectangle portBounds = getBounds(); if (viewBounds.width == 0 || viewBounds.height == 0 || portBounds.width == 0 || portBounds.height == 0) return; switch (getScrollMode()) { case JViewport.BACKINGSTORE_SCROLL_MODE: paintBackingStore(g); break; case JViewport.BLIT_SCROLL_MODE: paintBlit(g); break; case JViewport.SIMPLE_SCROLL_MODE: default: paintSimple(g); break; } damaged = false; } public void addChangeListener(ChangeListener listener) { listenerList.add(ChangeListener.class, listener); } public void removeChangeListener(ChangeListener listener) { listenerList.remove(ChangeListener.class, listener); } public ChangeListener[] getChangeListeners() { return (ChangeListener[]) getListeners(ChangeListener.class); } /** * This method returns the String ID of the UI class of Separator. * * @return The UI class' String ID. */ public String getUIClassID() { return "ViewportUI"; } /** * This method resets the UI used to the Look and Feel defaults.. */ public void updateUI() { setUI((ViewportUI) UIManager.getUI(this)); } /** * This method returns the viewport's UI delegate. * * @return The viewport's UI delegate. */ public ViewportUI getUI() { return (ViewportUI) ui; } /** * This method sets the viewport's UI delegate. * * @param ui The viewport's UI delegate. */ public void setUI(ViewportUI ui) { super.setUI(ui); } public final void setBorder(Border border) { if (border != null) throw new IllegalArgumentException(); } /** * Scrolls the view so that contentRect becomes visible. * * @param contentRect the rectangle to make visible within the view */ public void scrollRectToVisible(Rectangle contentRect) { Component view = getView(); if (view == null) return; Point pos = getViewPosition(); // We get the contentRect in the viewport coordinates. But we want to // calculate with view coordinates. int contentX = contentRect.x + pos.x; int contentY = contentRect.y + pos.y; Rectangle viewBounds = getView().getBounds(); Rectangle portBounds = getBounds(); if (isShowing()) getView().validate(); // If the bottom boundary of contentRect is below the port // boundaries, scroll up as necessary. if (contentY + contentRect.height + viewBounds.y > portBounds.height) pos.y = contentY + contentRect.height - portBounds.height; // If contentY is above the port boundaries, scroll down to // contentY. if (contentY + viewBounds.y < 0) pos.y = contentY; // If the right boundary of contentRect is right from the port // boundaries, scroll left as necessary. if (contentX + contentRect.width + viewBounds.x > portBounds.width) pos.x = contentX + contentRect.width - portBounds.width; // If contentX is left from the port boundaries, scroll right to // contentRect.x. if (contentX + viewBounds.x < 0) pos.x = contentX; setViewPosition(pos); } /** * Returns the accessible context for this JViewport. This * will be an instance of {@link AccessibleJViewport}. * * @return the accessible context for this JViewport */ public AccessibleContext getAccessibleContext() { if (accessibleContext == null) accessibleContext = new AccessibleJViewport(); return accessibleContext; } /** * Forward repaint to parent to make sure only one paint is performed by the * RepaintManager. * * @param tm number of milliseconds to defer the repaint request * @param x the X coordinate of the upper left corner of the dirty area * @param y the Y coordinate of the upper left corner of the dirty area * @param w the width of the dirty area * @param h the height of the dirty area */ public void repaint(long tm, int x, int y, int w, int h) { Component parent = getParent(); if (parent != null) parent.repaint(tm, x + getX(), y + getY(), w, h); else super.repaint(tm, x, y, w, h); } protected void addImpl(Component comp, Object constraints, int index) { if (getComponentCount() > 0) remove(getComponents()[0]); super.addImpl(comp, constraints, index); } protected void fireStateChanged() { ChangeListener[] listeners = getChangeListeners(); for (int i = 0; i < listeners.length; ++i) listeners[i].stateChanged(changeEvent); } /** * Creates a {@link ViewListener} that is supposed to listen for * size changes on the view component. * * @return a ViewListener instance */ protected ViewListener createViewListener() { return new ViewListener(); } /** * Creates the LayoutManager that is used for this viewport. Override * this method if you want to use a custom LayoutManager. * * @return a LayoutManager to use for this viewport */ protected LayoutManager createLayoutManager() { return new ViewportLayout(); } /** * Computes the parameters for the blitting scroll method. dx * and dy specifiy the X and Y offset by which the viewport * is scrolled. All other arguments are output parameters and are filled by * this method. * * blitFrom holds the position of the blit rectangle in the * viewport rectangle before scrolling, blitTo where the blitArea * is copied to. * * blitSize holds the size of the blit area and * blitPaint is the area of the view that needs to be painted. * * This method returns true if blitting is possible and * false if the viewport has to be repainted completetly without * blitting. * * @param dx the horizontal delta * @param dy the vertical delta * @param blitFrom the position from where to blit; set by this method * @param blitTo the position where to blit area is copied to; set by this * method * @param blitSize the size of the blitted area; set by this method * @param blitPaint the area that needs repainting; set by this method * * @return true if blitting is possible, * false otherwise */ protected boolean computeBlit(int dx, int dy, Point blitFrom, Point blitTo, Dimension blitSize, Rectangle blitPaint) { if ((dx != 0 && dy != 0) || (dy == 0 && dy == 0) || damaged) // We cannot blit if the viewport is scrolled in both directions at // once. Also, we do not want to blit if the viewport is not scrolled at // all, because that probably means the view component repaints itself // and the buffer needs updating. return false; Rectangle portBounds = SwingUtilities.calculateInnerArea(this, getBounds()); // Compute the blitFrom and blitTo parameters. blitFrom.x = portBounds.x; blitFrom.y = portBounds.y; blitTo.x = portBounds.x; blitTo.y = portBounds.y; if (dy > 0) { blitFrom.y = portBounds.y + dy; } else if (dy < 0) { blitTo.y = portBounds.y - dy; } else if (dx > 0) { blitFrom.x = portBounds.x + dx; } else if (dx < 0) { blitTo.x = portBounds.x - dx; } // Compute size of the blit area. if (dx != 0) { blitSize.width = portBounds.width - Math.abs(dx); blitSize.height = portBounds.height; } else if (dy != 0) { blitSize.width = portBounds.width; blitSize.height = portBounds.height - Math.abs(dy); } // Compute the blitPaint parameter. blitPaint.setBounds(portBounds); if (dy > 0) { blitPaint.y = portBounds.y + portBounds.height - dy; blitPaint.height = dy; } else if (dy < 0) { blitPaint.height = -dy; } if (dx > 0) { blitPaint.x = portBounds.x + portBounds.width - dx; blitPaint.width = dx; } else if (dx < 0) { blitPaint.width = -dx; } return true; } /** * Paints the viewport in case we have a scrollmode of * {@link #SIMPLE_SCROLL_MODE}. * * This simply paints the view directly on the surface of the viewport. * * @param g the graphics context to use */ void paintSimple(Graphics g) { // We need to call this to properly clear the background. paintComponent(g); Point pos = getViewPosition(); Component view = getView(); Shape oldClip = g.getClip(); g.clipRect(0, 0, getWidth(), getHeight()); boolean translated = false; try { g.translate(-pos.x, -pos.y); translated = true; view.paint(g); } finally { if (translated) g.translate (pos.x, pos.y); g.setClip(oldClip); } } /** * Paints the viewport in case we have a scroll mode of * {@link #BACKINGSTORE_SCROLL_MODE}. * * This method uses a backing store image to paint the view to, which is then * subsequently painted on the screen. This should make scrolling more * smooth. * * @param g the graphics context to use */ void paintBackingStore(Graphics g) { // If we have no backing store image yet or the size of the component has // changed, we need to rebuild the backing store. if (backingStoreImage == null || sizeChanged) { backingStoreImage = createImage(getWidth(), getHeight()); sizeChanged = false; Graphics g2 = backingStoreImage.getGraphics(); paintSimple(g2); g2.dispose(); } // Otherwise we can perform the blitting on the backing store image: // First we move the part that remains visible after scrolling, then // we only need to paint the bit that becomes newly visible. else { Graphics g2 = backingStoreImage.getGraphics(); Point viewPosition = getViewPosition(); int dx = viewPosition.x - lastPaintPosition.x; int dy = viewPosition.y - lastPaintPosition.y; boolean canBlit = computeBlit(dx, dy, cachedBlitFrom, cachedBlitTo, cachedBlitSize, cachedBlitPaint); if (canBlit && isPaintRoot) { // Copy the part that remains visible during scrolling. if (cachedBlitSize.width > 0 && cachedBlitSize.height > 0) { g2.copyArea(cachedBlitFrom.x, cachedBlitFrom.y, cachedBlitSize.width, cachedBlitSize.height, cachedBlitTo.x - cachedBlitFrom.x, cachedBlitTo.y - cachedBlitFrom.y); } // Now paint the part that becomes newly visible. g2.setClip(cachedBlitPaint.x, cachedBlitPaint.y, cachedBlitPaint.width, cachedBlitPaint.height); paintSimple(g2); } // If blitting is not possible for some reason, fall back to repainting // everything. else { // If the image has not been scrolled at all, only the changed // clip must be updated in the buffer. if (dx == 0 && dy == 0) g2.setClip(g.getClip()); paintSimple(g2); } g2.dispose(); } // Actually draw the backingstore image to the graphics context. g.drawImage(backingStoreImage, 0, 0, this); // Update the lastPaintPosition so that we know what is already drawn when // we paint the next time. lastPaintPosition.setLocation(getViewPosition()); } /** * Paints the viewport in case we have a scrollmode of * {@link #BLIT_SCROLL_MODE}. * * This paints the viewport using a backingstore and a blitting algorithm. * Only the newly exposed area of the view is painted from the view painting * methods, the remainder is copied from the backing store. * * @param g the graphics context to use */ void paintBlit(Graphics g) { // First we move the part that remains visible after scrolling, then // we only need to paint the bit that becomes newly visible. Point viewPosition = getViewPosition(); int dx = viewPosition.x - lastPaintPosition.x; int dy = viewPosition.y - lastPaintPosition.y; boolean canBlit = computeBlit(dx, dy, cachedBlitFrom, cachedBlitTo, cachedBlitSize, cachedBlitPaint); if (canBlit && isPaintRoot) { // Copy the part that remains visible during scrolling. if (cachedBlitSize.width > 0 && cachedBlitSize.width > 0) { g.copyArea(cachedBlitFrom.x, cachedBlitFrom.y, cachedBlitSize.width, cachedBlitSize.height, cachedBlitTo.x - cachedBlitFrom.x, cachedBlitTo.y - cachedBlitFrom.y); } // Now paint the part that becomes newly visible. Shape oldClip = g.getClip(); g.clipRect(cachedBlitPaint.x, cachedBlitPaint.y, cachedBlitPaint.width, cachedBlitPaint.height); try { paintSimple(g); } finally { g.setClip(oldClip); } } // If blitting is not possible for some reason, fall back to repainting // everything. else paintSimple(g); lastPaintPosition.setLocation(getViewPosition()); } /** * Overridden from JComponent to set the {@link #isPaintRoot} flag. * * @param x the rectangle to paint, X coordinate * @param y the rectangle to paint, Y coordinate * @param w the rectangle to paint, width * @param h the rectangle to paint, height */ void paintImmediately2(int x, int y, int w, int h) { isPaintRoot = true; super.paintImmediately2(x, y, w, h); isPaintRoot = false; } /** * Returns true when the JViewport is using a backbuffer, so that we * can update our backbuffer correctly. */ boolean isPaintRoot() { return scrollMode == BACKINGSTORE_SCROLL_MODE; } }