package edu.princeton.swing; import java.awt.*; import javax.swing.*; /** * PScrollablePanel is just like a JPanel except that it implements scrollable. * * @author btsang * @version 7.1 */ public class PScrollablePanel extends JPanel implements Scrollable { /** * The tracking policy whereby this PScrollablePanel will always track JViewport's width/height. * * @see #setWidthTrackingPolicy(byte) * @see #setHeightTrackingPolicy(byte) */ public static final byte TRACKING_POLICY_NEVER = (byte)0; /** * The tracking policy whereby this PScrollablePanel will track JViewport's width/height only * if will cause the panel to expand. * * @see #setWidthTrackingPolicy(byte) * @see #setHeightTrackingPolicy(byte) */ public static final byte TRACKING_POLICY_IF_WILL_EXPAND = (byte)1; /** * The tracking policy whereby this PScrollablePanel will never track JViewport's width/height. * * @see #setWidthTrackingPolicy(byte) * @see #setHeightTrackingPolicy(byte) */ public static final byte TRACKING_POLICY_ALWAYS = (byte)2; private Dimension preferredScrollableViewportSize; private byte widthTrackingPolicy; private byte heightTrackingPolicy; /** * Creates a new PScrollablePanel. */ public PScrollablePanel() { super(); initPanel(); } /** * Creates a new PScrollablePanel. */ public PScrollablePanel(boolean isDoubleBuffered) { super(isDoubleBuffered); initPanel(); } /** * Creates a new PScrollablePanel. */ public PScrollablePanel(LayoutManager layout) { super(layout); initPanel(); } /** * Creates a new PScrollablePanel. */ public PScrollablePanel(LayoutManager layout, boolean isDoubleBuffered) { super(layout, isDoubleBuffered); initPanel(); } /** * Called by the constructors to init the PScrollablePanel properly. */ private void initPanel() { preferredScrollableViewportSize = null; widthTrackingPolicy = TRACKING_POLICY_IF_WILL_EXPAND; heightTrackingPolicy = TRACKING_POLICY_IF_WILL_EXPAND; } /** * Returns the preferred size of the viewport for a view component. For example the * preferredSize of a JList component is the size required to acommodate all of the cells in * its list however the value of preferredScrollableViewportSize is the size required for * JList.getVisibleRowCount() rows. A component without any properties that would effect the * viewport size should just return getPreferredSize() here. * * @return The preferredSize of a JViewport whose view is this Scrollable. * @see #getActualPreferredScrollableViewportSize() * @see #setPreferredScrollableViewportSize(Dimension) */ public Dimension getPreferredScrollableViewportSize() { if (preferredScrollableViewportSize == null) return getPreferredSize(); else return preferredScrollableViewportSize; } /** * Returns the value passed to setPreferredScrollableViewportSize(). A null value indicates * that getPreferredScrollableViewportSize() should return getPreferredSize(). * * @return The value passed to setPreferredScrollableViewportSize(). * @see #getPreferredScrollableViewportSize() * @see #setPreferredScrollableViewportSize(Dimension) */ public Dimension getActualPreferredScrollableViewportSize() { return preferredScrollableViewportSize; } /** * Allows the user to set what getPreferredScrollableViewportSize() will return. A null * value indicates that such calls will be passed to getPreferredSize(). Null is the default. * * @param preferredScrollableViewportSize The new preferred size of the viewport. A null * value indicates that such calls will be passed to getPreferredSize(). * @see #getPreferredScrollableViewportSize() * @see #setPreferredScrollableViewportSize(Dimension) */ public void setPreferredScrollableViewportSize(Dimension preferredScrollableViewportSize) { if (this.preferredScrollableViewportSize != preferredScrollableViewportSize && (preferredScrollableViewportSize != null || !preferredScrollableViewportSize.equals(this.preferredScrollableViewportSize))) { Dimension oldValue = this.preferredScrollableViewportSize; this.preferredScrollableViewportSize = preferredScrollableViewportSize; firePropertyChange( "preferredScrollableViewportSize", oldValue, preferredScrollableViewportSize ); } } /** * Components that display logical rows or columns should compute the scroll increment that will * completely expose one new row or column, depending on the value of orientation. Ideally, * components should handle a partially exposed row or column by returning the distance * required to completely expose the item. *

Scrolling containers, like JScrollPane, will use this method each time the user requests * a unit scroll. *

PScrollablePanel's implementation of this function attempts to see if the relevant * child of the panel is a Scrollable. If not, it will default to a tenth of the visible * screen. * * @param visibleRect The view area visible within the viewport. * @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL. * @param direction Less than zero to scroll up/left, greater than zero for down/right. * @return The "block" increment for scrolling in the specified direction. */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { Component component = null; // Try to let our scrollable children answer the question if (orientation == SwingConstants.VERTICAL) { if (direction < 0) { // Scrolling up, see if the component just beyond the top edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x + visibleRect.width / 2, visibleRect.y - 1 ); } else { // Scrolling down, see if the component just beyond the bottom edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height + 1 ); } } else { if (direction < 0) { // Scrolling left, see if the component just beyond the left edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x - 1, visibleRect.y + visibleRect.height / 2 ); } else { // Scrolling right, see if the component just beyond the right edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x + visibleRect.width + 1, visibleRect.y + visibleRect.height / 2 ); } } if (component != null && component instanceof Scrollable) { Scrollable scrollable = (Scrollable)component; int x = component.getX(); int y = component.getY(); visibleRect.x -= x; visibleRect.y -= y; int answer = scrollable.getScrollableUnitIncrement( visibleRect, orientation, direction ); visibleRect.x += x; visibleRect.y += y; return answer; } // Our default response is a tenth of the visible screen if (orientation == SwingConstants.VERTICAL) return visibleRect.height / 10; else return visibleRect.width / 10; } /** * Components that display logical rows or columns should compute the scroll increment that * will completely expose one block of rows or columns, depending on the value of orientation. *

Scrolling containers, like JScrollPane, will use this method each time the user requests * a block scroll. *

PScrollablePanel's implementation of this function attempts to see if the relevant * child of the panel is a Scrollable. If not, it will default to a full visible screen. * * @param visibleRect The view area visible within the viewport. * @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL. * @param direction Less than zero to scroll up/left, greater than zero for down/right. * @return The "block" increment for scrolling in the specified direction. */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { Component component = null; // Try to let our scrollable children answer the question if (orientation == SwingConstants.VERTICAL) { if (direction < 0) { // Scrolling up, see if the component just beyond the top edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x + visibleRect.width / 2, visibleRect.y - 1 ); } else { // Scrolling down, see if the component just beyond the bottom edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height + 1 ); } } else { if (direction < 0) { // Scrolling left, see if the component just beyond the left edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x - 1, visibleRect.y + visibleRect.height / 2 ); } else { // Scrolling right, see if the component just beyond the right edge of the visible // rectangle is a Scrollable component component = getComponentAt( visibleRect.x + visibleRect.width + 1, visibleRect.y + visibleRect.height / 2 ); } } if (component != null && component instanceof Scrollable) { Scrollable scrollable = (Scrollable)component; int x = component.getX(); int y = component.getY(); visibleRect.x -= x; visibleRect.y -= y; int answer = scrollable.getScrollableBlockIncrement( visibleRect, orientation, direction ); visibleRect.x += x; visibleRect.y += y; return answer; } // Our default response is a full visible screen if (orientation == SwingConstants.VERTICAL) return visibleRect.height; else return visibleRect.width; } /** * Returns the width-tracking policy of the PScrollablePanel. This policy determines the * return value of getScrollableTracksViewportWidth(). * * @return The width-tracking policy of the PScrollablePanel. * @see #TRACKING_POLICY_NEVER * @see #TRACKING_POLICY_IF_WILL_EXPAND * @see #TRACKING_POLICY_ALWAYS */ public byte getWidthTrackingPolicy() { return widthTrackingPolicy; } /** * Sets the width-tracking policy of the PScrollablePanel. This policy determines the * return value of getScrollableTracksViewportWidth(). The default value is * TRACKING_POLICY_IF_WILL_EXPAND. * * @param widthTrackingPolicy The width-tracking policy of the PScrollablePanel. An * IllegalArgumentException will be thrown if this is not valid. * @see #TRACKING_POLICY_NEVER * @see #TRACKING_POLICY_IF_WILL_EXPAND * @see #TRACKING_POLICY_ALWAYS */ public void setWidthTrackingPolicy(byte widthTrackingPolicy) { if (widthTrackingPolicy != TRACKING_POLICY_NEVER && widthTrackingPolicy != TRACKING_POLICY_IF_WILL_EXPAND && widthTrackingPolicy != TRACKING_POLICY_ALWAYS) throw new IllegalArgumentException(); if (this.widthTrackingPolicy != widthTrackingPolicy) { byte oldValue = this.widthTrackingPolicy; this.widthTrackingPolicy = widthTrackingPolicy; firePropertyChange( "widthTrackingPolicy", oldValue, widthTrackingPolicy ); } } /** * Return true if a viewport should always force the width of this Scrollable to match the * width of the viewport. For example a noraml text view that supported line wrapping would * return true here, since it would be undesirable for wrapped lines to disappear beyond the * right edge of the viewport. Note that returning true for a Scrollable whose ancestor is a * JScrollPane effectively disables horizontal scrolling. *

Scrolling containers, like JViewport, will use this method each time they are validated. * * @return True if a viewport should force the Scrollables width to match its own. */ public boolean getScrollableTracksViewportWidth() { if (widthTrackingPolicy == TRACKING_POLICY_NEVER) { return false; } else if (widthTrackingPolicy == TRACKING_POLICY_IF_WILL_EXPAND) { if (getParent() instanceof JViewport) return (((JViewport)getParent()).getWidth() > getPreferredSize().width); else return false; } else { return true; } } /** * Returns the height-tracking policy of the PScrollablePanel. This policy determines the * return value of getScrollableTracksViewportHeight(). * * @return The height-tracking policy of the PScrollablePanel. * @see #TRACKING_POLICY_NEVER * @see #TRACKING_POLICY_IF_WILL_EXPAND * @see #TRACKING_POLICY_ALWAYS */ public byte getHeightTrackingPolicy() { return heightTrackingPolicy; } /** * Sets the height-tracking policy of the PScrollablePanel. This policy determines the * return value of getScrollableTracksViewportHeight(). The default value is * TRACKING_POLICY_IF_WILL_EXPAND. * * @param heightTrackingPolicy The height-tracking policy of the PScrollablePanel. An * IllegalArgumentException will be thrown if this is not valid. * @see #TRACKING_POLICY_NEVER * @see #TRACKING_POLICY_IF_WILL_EXPAND * @see #TRACKING_POLICY_ALWAYS */ public void setHeightTrackingPolicy(byte heightTrackingPolicy) { if (heightTrackingPolicy != TRACKING_POLICY_NEVER && heightTrackingPolicy != TRACKING_POLICY_IF_WILL_EXPAND && heightTrackingPolicy != TRACKING_POLICY_ALWAYS) throw new IllegalArgumentException(); if (this.heightTrackingPolicy != heightTrackingPolicy) { byte oldValue = this.heightTrackingPolicy; this.heightTrackingPolicy = heightTrackingPolicy; firePropertyChange( "heightTrackingPolicy", oldValue, heightTrackingPolicy ); } } /** * Return true if a viewport should always force the height of this Scrollable to match the * height of the viewport. For example a columnar text view that flowed text in left to right * columns could effectively disable vertical scrolling by returning true here. *

Scrolling containers, like JViewport, will use this method each time they are validated. * * @return True if a viewport should force the Scrollables height to match its own. */ public boolean getScrollableTracksViewportHeight() { if (heightTrackingPolicy == TRACKING_POLICY_NEVER) { return false; } else if (heightTrackingPolicy == TRACKING_POLICY_IF_WILL_EXPAND) { if (getParent() instanceof JViewport) return (((JViewport)getParent()).getHeight() > getPreferredSize().height); else return false; } else { return true; } } }