Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.globalmentor.swing.BasicPanel Maven / Gradle / Ivy
/*
* Copyright © 1996-2009 GlobalMentor, Inc.
*
* 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 com.globalmentor.swing;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.util.prefs.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import com.globalmentor.awt.*;
import com.globalmentor.java.*;
import com.globalmentor.java.Objects;
import com.globalmentor.model.Verifiable;
import com.globalmentor.swing.event.*;
import static com.globalmentor.awt.Containers.*;
import static com.globalmentor.java.Arrays.*;
/**
* An extended panel that has extra features beyond those in {@link JPanel}.
*
* The panel stores properties and fires property change events when a property is modified.
*
*
* The panel is verifiable, and automatically verifies all child components that are likewise verifiable.
*
*
* The panel can store a preferences node to use for preference, or use the default preferences node for the panel class.
*
*
* The panel keeps a lazily-created manager that manages menu and tool actions.
*
*
* The panel keeps track of its current user mode and view of data.
*
*
* The panel can indicate whether it can close.
*
*
* The panel can recognize when it is embedded in a JOptionPane
and can set certain option pane values accordingly.
*
*
* The panel can keep track of a title, and if the panel has a titled border, it will automatically update the border's title when the title changes.
*
*
* The panel can keep track of which child component should get the default focus. An extended focus traversal policy is installed so that, if this panel
* because a root focus traversal cycle, the correct default focus component will be selected. This implementation recognizes tabbed panes and automatically
* delegates to the selected tab if the tabbed pane has been specified as the default focus component.
*
*
* The panel can create default listeners, such as ActionListener
and DocumentListener
, that do nothing but update the status.
*
*
* The panel is scrollable, and by default takes care of its own width and/or height rather than allowing any parent scroll width and height. This behavior can
* be modified.
*
*
* When this panel is enabled or disabled, it updates its status through updateStatus()
.
*
*
* The panel keeps an event listener list for convenient adding and removal of specific event listener types.
*
*
* Bound properties:
*
*
* BasicPanel{@link #ICON_PROPERTY} (Icon
)
* Indicates the icon has been changed.
* BasicPanel{@link #TITLE_PROPERTY} (String
)
* Indicates the title has been changed.
* BasicPanel{@link #USER_MODE_PROPERTY} (Integer
)
* Indicates the user mode has been changed.
*
* @author Garret Wilson
* @see Container#setFocusCycleRoot(boolean)
* @see PropertyChangeListener
* @see JOptionPane
* @see GridBagLayout
*/
public class BasicPanel extends JPanel implements Scrollable, CanClosable, DefaultFocusable, Verifiable, ActionManaged {
/** The name of the bound icon property. */
public static final String ICON_PROPERTY = BasicPanel.class.getName() + Java.PACKAGE_SEPARATOR + "icon";
/** The name of the bound title property. */
public static final String TITLE_PROPERTY = BasicPanel.class.getName() + Java.PACKAGE_SEPARATOR + "title"; //TODO maybe later move this to a titleable interface
/** The name of the bound user mode property. */
public static final String USER_MODE_PROPERTY = BasicPanel.class.getName() + Java.PACKAGE_SEPARATOR + "userMode";
/**
* The preferences that should be used for this panel, or null
if the default preferences for this class should be used.
*/
private Preferences preferences;
/**
* @return The preferences that should be used for this panel, or the default preferences for this class if no preferences are specifically set.
* @throws SecurityException Thrown if a security manager is present and it denies RuntimePermission("preferences")
.
*/
public Preferences getPreferences() throws SecurityException {
return preferences != null ? preferences : Preferences.userNodeForPackage(getClass()); //return the user preferences node for whatever class extends this one
}
/**
* Sets the preferences to be used for this panel.
* @param preferences The preferences that should be used for this panel, or null
if the default preferences for this class should be used
*/
public void setPreferences(final Preferences preferences) {
this.preferences = preferences; //store the preferences
}
/** The map of properties. */
private final Map propertyMap = new HashMap();
/**
* Gets a property of the panel.
* @param key The key to the property.
* @return The value of the panel's property, or null
if that property does not exist.
*/
public Object getProperty(final Object key) {
return propertyMap.get(key); //return the property from the property map
}
/**
* Sets the value of a panel property, and fires a property changed event if the key is a string. If the property represented by the key already exists, it
* will be replaced.
* @param key The non-null
property key.
* @param value The property value.
* @return The old property value associated with the key, or null
if no value was associated with the key previously.
* @see PropertyChangeEvent
*/
public Object setProperty(final Object key, final Object value) {
final Object oldValue = propertyMap.put(key, value); //put the value in the map keyed to the key and save the old value
if(key instanceof String) { //if they key was a string
firePropertyChange((String)key, oldValue, value); //show that the property value has changed
}
return oldValue; //return the old property value, if there was one
}
/**
* Removes a property of the panel. If the property represented by the key does not exist, no action is taken.
* @param key The non-null
property key.
* @return The removed property value, or null
if there was no property.
*/
public Object removeProperty(final Object key) {
return propertyMap.remove(key); //remove and return the property value keyed to the key
}
/** The lazily-created manager of menu and tool actions. */
private ActionManager actionManager;
/** @return The lazily-created manager of menu and tool actions. */
public ActionManager getActionManager() {
if(actionManager == null) { //if we haven't yet created an action manager
actionManager = new ActionManager(); //create a new action manager
}
return actionManager; //return the action manager
}
/** The title of the panel, or null
if there is no title. */
private String title = null;
/** @return The title of the panel, or null
if there is no title. */
public String getTitle() {
return title;
}
/**
* Sets the title of the panel. If the panel border is a TitledBorder
, its title is updated. This is a bound property.
* @param newTitle The new title of the panel, or null
for no title.
*/
public void setTitle(final String newTitle) {
final String oldTitle = title; //get the old title value
final Border border = getBorder(); //get our current border
if(border instanceof TitledBorder) { //if the border is a titled border
final TitledBorder titledBorder = (TitledBorder)border; //cast the border to a titled border
if(!Objects.equals(titledBorder.getTitle(), newTitle)) { //if the new title is different than the one currently on the border
titledBorder.setTitle(newTitle); //update the title on the border
}
}
if(!Objects.equals(oldTitle, newTitle)) { //if the value is really changing
title = newTitle; //update the value
firePropertyChange(TITLE_PROPERTY, oldTitle, newTitle); //show that the property has changed
}
}
/** The icon of the panel, or null
if there is no icon. */
private Icon icon = null;
/** @return The icon of the panel, or null
if there is no icon. */
public Icon getIcon() {
return icon;
}
/**
* Sets the icon of the panel. This is a bound property.
* @param newIcon The new icon of the panel, or null
for no icon.
*/
public void setIcon(final Icon newIcon) {
final Icon oldIcon = icon; //get the old title value
if(oldIcon != newIcon) { //if the value is really changing
icon = newIcon; //update the value
firePropertyChange(ICON_PROPERTY, oldIcon, newIcon); //show that the property has changed
}
}
/** The component that should get the default focus, or null
if unknown. */
private Component defaultFocusComponent;
/**
* @return The component that should get the default focus, or null
if no component should get the default focus or it is unknown which component
* should get the default focus.
*/
public Component getDefaultFocusComponent() {
return defaultFocusComponent;
}
/**
* Sets the component to get the focus by default. If this panel becomes a root focus traversal cycle, the default installed focus traversal policy will
* automatically allow this component to get the default focus.
* @param component The component to get the default focus.
*/
public void setDefaultFocusComponent(final Component component) {
defaultFocusComponent = component;
}
/** The mode in which the user can best view the contents of the panel. */
public static final int VIEW_MODE = 1 << 0;
/** The mode in which the user can best modify the contents of the panel. */
public static final int EDIT_MODE = 1 << 1;
/** The mode in which the user can interact with the contents of the panel. */
public static final int INTERACT_MODE = 1 << 2;
/** The mode of interaction with the user, such as EDIT_MODE
. */
private int userMode;
/** @return The mode of interaction with the user, such as EDIT_MODE
. */
public int getUserMode() {
return userMode;
}
/**
* Sets the mode of user interaction.
* @param newUserMode The mode of interaction with the user, such as EDIT_MODE
.
*/
public void setUserMode(final int newUserMode) {
final int oldUserMode = userMode; //get the old value
if(oldUserMode != newUserMode) { //if the value is really changing
userMode = newUserMode; //update the value
firePropertyChange(USER_MODE_PROPERTY, new Integer(oldUserMode), new Integer(newUserMode)); //show that the property has changed
}
}
/**
* Sets whether or not this component is enabled. This implementation enables calls updateStatus()
if the enabled status changes.
* @param enabled true
if this component should be enabled, false
otherwise.
* @see #updateStatus()
*/
public void setEnabled(final boolean enabled) {
final boolean oldEnabled = isEnabled(); //see whether we're currently enabled
super.setEnabled(enabled); //do the default enabling
if(oldEnabled != enabled) { //if the enabled state changed
updateStatus(); //update the status
}
}
/** The lazily-created list of event listeners. */
private EventListenerList eventListenerList = null;
/** @return The lazily-created list of event listeners. */
private EventListenerList getEventListenerList() {
if(eventListenerList == null) { //if there is yet no event listener list
eventListenerList = new EventListenerList(); //create a new event listener list
}
return eventListenerList; //return the event listener list
}
/**
* Adds the listener as a listener of the specified type.
* @param eventListenerType The type of the listener to be added.
* @param eventListener the listener to be added
*/
protected void addEventListener(final Class eventListenerType, T eventListener) {
getEventListenerList().add(eventListenerType, eventListener); //add the listener to the list of event listeners
}
/**
* Removes the listener as a listener of the specified type.
* @param eventListenerType the type of the listener to be removed
* @param eventListener the listener to be removed
*/
public void removeEventListener(final Class eventListenerType, T eventListener) {
getEventListenerList().remove(eventListenerType, eventListener); //remove the listener from the list of event listeners
}
/**
* @return An array of all the listeners of the given type.
* @throws ClassCastException if the supplied class is not assignable to EventListener
.
*/
protected T[] getEventListeners(final Class eventListenerType) {
if(eventListenerList != null) { //if we have a list of event listeners
return eventListenerList.getListeners(eventListenerType); //ask the list of event listeners to send back all event listeners of the given type
} else { //if we have no list of event listeners
return createArray(eventListenerType, 0); //send back an empty array
}
}
/**
* Default constructor that uses a FlowLayout
.
* @see #FlowLayout
*/
public BasicPanel() {
this(true); //initialize the panel
}
/**
* Constructor with optional initialization that uses a FlowLayout
.
* @param initialize true
if the panel should initialize itself by calling the initialization methods.
* @see #FlowLayout
*/
public BasicPanel(final boolean initialize) {
this(new FlowLayout(), initialize); //construct the panel with a flow layout by default
}
/**
* Layout constructor.
* @param layout The layout manager to use.
*/
public BasicPanel(final LayoutManager layout) {
this(layout, true); //construct the class with the layout, initializing the panel
}
/**
* Layout constructor with optional initialization.
* @param layout The layout manager to use.
* @param initialize true
if the panel should initialize itself by calling the initialization methods.
*/
public BasicPanel(final LayoutManager layout, final boolean initialize) {
super(layout, false); //construct the parent class but don't initialize
preferences = null; //show that we should use the default preferences for this class
actionManager = null; //default to no action manager until one is asked for
userMode = VIEW_MODE; //default to viewing the data
defaultFocusComponent = null; //default to no default focus component
//create and install a new layout focus traversal policy that will
//automatically use the default focus component, if available
setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
public Component getDefaultComponent(final Container focusCycleRoot) { //if the default component is requested
Component defaultFocusComponent = getDefaultFocusComponent(); //see if we have a default focus component
/*TODO fix
if(defaultFocusComponent instanceof JTabbedPane) { //if the default focus component is a tabbed pane //TODO check; doesn't seem to work
final Component selectedTab=((JTabbedPane)defaultFocusComponent).getSelectedComponent(); //get the selected tab
if(selectedTab!=null) { //if there is a selected tab
defaultFocusComponent=selectedTab instanceof DefaultFocusable //if the tab component knows about default focus components
? ((DefaultFocusable)selectedTab).getDefaultFocusComponent() //ask the tab for a default focus component
: selectedTab; //otherwise, use the selected tab as the default focus component
}
}
*/
//if we have a default focus component, return it; otherwise, use the value given by the parent traversal policy class
return defaultFocusComponent != null ? defaultFocusComponent : super.getDefaultComponent(focusCycleRoot);
}
});
if(initialize) //if we should initialize
initialize(); //initialize the panel
}
/**
* Initializes the panel. Should only be called once per instance.
* @see #initializeUI
*/
public void initialize() { //TODO set a flag that will only allow initialization once per instance
initializeActions(getActionManager()); //initialize actions
initializeUI(); //initialize the user interface
initializeData(); //initialize the data
updateStatus(); //update the actions
}
/**
* Initializes actions in the action manager. Any derived class that overrides this method should call this version.
* @param actionManager The implementation that manages actions.
*/
protected void initializeActions(final ActionManager actionManager) {
}
/**
* Initializes the user interface. Any derived class that overrides this method should call this version.
*/
protected void initializeUI() {
}
/**
* Initializes the data. Any derived class that overrides this method should call this version.
*/
protected void initializeData() {
}
/** Updates the states of the user interface, including enabled/disabled status, proxied actions, etc. */
public void updateStatus() {
}
/**
* Requests that the default focus component should get the default.
*
* If the component is a tab in a tabbed pane, that tab in the tabbed pane is selected.
*
*
* If the default focus comonent is itself DefaultFocusable
, that component is asked to request focus for its default focus component, and so on.
*
* @return false
if the focus change request is guaranteed to fail; true
if it is likely to succeed.
* @see Component#requestFocusInWindow
*/
public boolean requestDefaultFocusComponentFocus() {
final Component defaultFocusComponent = getDefaultFocusComponent(); //get the default focus component
if(defaultFocusComponent != null) { //if there is a default focus component, make sure its parent tabs are selected if it's in a tabbed pane
TabbedPaneUtilities.setSelectedParentTabs(defaultFocusComponent); //select the tabs of any parent tabbed panes
}
if(defaultFocusComponent instanceof DefaultFocusable //if the component is itself default focusable
&& ((DefaultFocusable)defaultFocusComponent).getDefaultFocusComponent() != defaultFocusComponent) { //and the default focus component does not reference itself (which would create an endless loop)
return ((DefaultFocusable)defaultFocusComponent).requestDefaultFocusComponentFocus(); //pass the request on to the default focus component
} else if(defaultFocusComponent != null) { //if the default focus component doesn't itself know about default focus components, but there is a default focus component
return defaultFocusComponent.requestFocusInWindow(); //tell the default focus component to request the focus
} else { //if there is no default focus component
return false; //there was nothing to focus
}
}
/** @return true
if the panel can close. */
public boolean canClose() {
return true; //default to always allowing closing
}
/**
* Verifies the component.
*
* This version verifies all desdendant components that implement Verifiable
, and returns true
if no child component returns
* false
.
*
* @return true
if the component contents are valid, false
if not.
*/
public boolean verify() {
return verifyDescendants(this); //verify all descendant components
}
/** @return The component that should get the initial focus. */
//TODO fix public Component getInitialFocusComponent() {return labelTextField;}
/**
* @return The JOptionPane
in which this panel is embedded, or null
if this panel is not embedded in a JOptionPane
.
*/
protected JOptionPane getParentOptionPane() {
Container parent = getParent(); //get the parent container
while(parent != null && !(parent instanceof JOptionPane)) { //while we're still getting parents, but we haven't found an option pane
parent = parent.getParent(); //get the parent's parent
}
return (JOptionPane)parent; //return the JOptionPane parent, or null if there was no JOptionPane parent
}
/**
* Sets the value property of the parent container JOptionPane
. If this panel is not embedded in a JOptionPane
, no action occurs.
*
* For example, setting a value of new Integer(JOptionPane.OK_OPTION)
will close the option pane and return that value.
*
* @param newValue The chosen value.
* @see JOptionPane#setValue
* @see #getValue
*/
public void setOptionPaneValue(final Object newValue) {
final JOptionPane optionPane = getParentOptionPane(); //get the option pane in which we're embedded
if(optionPane != null) { //if we're embedded in an option pane
optionPane.setValue(newValue); //set the value of the option pane
}
}
/**
* Returns the value the user has selected in the parent container JOptionPane
. UNINITIALIZED_VALUE
implies the user has not yet
* made a choice, null
means the user closed the window with out choosing anything or this panel is not embedded in a JOptionPane
.
* Otherwise the returned value should be one of the options defined in JOptionPane
.
* @return the Object
chosen by the user, UNINITIALIZED_VALUE
if the user has not yet made a choice, or null
if the
* user closed the window without making a choice or this panel is not embedded in a JOptionPane
.
* @see JOptionPane#getValue
* @see #setValue
*/
public Object getOptionPaneValue() {
final JOptionPane optionPane = getParentOptionPane(); //get the option pane in which we're embedded
return optionPane != null ? optionPane.getValue() : null; //return the value property of the option pane, or null if we are not embedded in a JOptionPane
}
/**
* Creates a list selection listener that, when the list selection changes, updates the status.
* @see #updateStatus
*/
public ListSelectionListener createUpdateStatusListSelectionListener() {
return new ListSelectionListener() { //create a new list selection listener that will do nothing but update the status
public void valueChanged(final ListSelectionEvent listSelectionEvent) {
updateStatus();
} //if the list selection changes, update the status
};
}
/**
* Creates a property change listener that, when any property changes, updates the modified status to true
.
* @see #setModified
*/
/*TODO bring back
public PropertyChangeListener createModifyPropertyChangeListener()
{
return new PropertyChangeListener() { //create a new property change listener that will do nothing but set modified to true
public void propertyChange(final PropertyChangeEvent propertyChangeEvent) {setModified(true);} //if a property is modified, show that we've been modified
};
}
*/
/**
* Creates a property change listener that, when a property chnages, updates the status.
* @see #updateStatus
*/
public PropertyChangeListener createUpdateStatusPropertyChangeListener() {
return new PropertyChangeListener() { //create a new property change listener that will do nothing but update the status
public void propertyChange(final PropertyChangeEvent propertyChangeEvent) {
updateStatus();
} //if a property is modified, update the status
};
}
/**
* Creates an action listener that, when an action occurs, updates the status.
* @see #updateStatus
*/
/*TODO bring back; or maybe we don't need, now that we have createUpdateStatusItemListener()
public ActionListener createUpdateStatusActionListener()
{
return new ActionListener() { //create a new action listener that will do nothing but update the status
public void actionPerformed(final ActionEvent actionEvent) {updateStatus();} //if the action occurs, update the status
};
}
*/
/**
* Creates a document listener that, when a document is modified, updates the status.
* @see #updateStatus
*/
public DocumentListener createUpdateStatusDocumentListener() {
return new DocumentModifyAdapter() { //create a new document listener that will do nothing but update the status
public void modifyUpdate(final DocumentEvent documentEvent) {
updateStatus();
} //if the document is modified, update the status
};
}
/**
* Creates an item listener that, when an item state changes, updates the status.
* @see #updateStatus
*/
public ItemListener createUpdateStatusItemListener() {
return new ItemListener() { //create a new item listener that will do nothing but update the status
public void itemStateChanged(final ItemEvent itemEvent) {
updateStatus();
} //if an item state changes, update the status
};
}
/**
* Creates a list data listener that, when a list model is modified, updates the status.
* @see #updateStatus
*/
public ListDataListener createUpdateStatusListDataListener() {
return new ListDataListener() { //create a new list data listener that will do nothing but update the status in response to changes
public void intervalAdded(final ListDataEvent listDataEvent) {
updateStatus();
}
public void intervalRemoved(final ListDataEvent listDataEvent) {
updateStatus();
}
public void contentsChanged(final ListDataEvent listDataEvent) {
updateStatus();
}
};
}
//Scrollable methods
/**
* Returns the preferred size of the viewport for a view component.
*
* This version simply returns the component's preferred size.
*
* @return The preferred panel size when scrolling.
* @see Panel#getPreferredSize
*/
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize(); //return the default preferred size
}
/**
* Determines the increment for unit scrolling.
* @param visibleRect The view area visible within the viewport.
* @param orientation The orientation, either SwingConstants.VERTICAL
or SwingConstants.HORIZONTAL
.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
* @return The unit increment for scrolling in the specified direction.
*/
public int getScrollableUnitIncrement(final Rectangle visibleRect, final int orientation, final int direction) {
switch(orientation) { //see on which axis we're being scrolled
case SwingConstants.VERTICAL:
return visibleRect.height / 10;
case SwingConstants.HORIZONTAL:
return visibleRect.width / 10;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
/**
* Determines the increment for block scrolling.
* @param visibleRect The view area visible within the viewport.
* @param orientation The 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) {
switch(orientation) { //see on which axis we're being scrolled
case SwingConstants.VERTICAL:
return visibleRect.height;
case SwingConstants.HORIZONTAL:
return visibleRect.width;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
/** Whether a viewport should always force the width of this panel to match its own. */
private boolean tracksViewportWidth = false;
/**
* Sets whether a viewport should always force the width of this panel to match its own width, unless the panel at its smallest could not fit inside the
* viewport.
* @param horizontallySelfManaged true
if the panel should not be scrolled horizontally by any parent viewport, allowing this panel to
* manage its own horizontal scrolling and/or wrapping.
*/
public void setTracksViewportWidth(final boolean horizontallySelfManaged) {
tracksViewportWidth = horizontallySelfManaged;
}
/**
* Returns whether a viewport should always force the width of this panel to match the width of the viewport.
*
* This implementation defaults to false
, allowing the viewport to scroll horizontally as needed.
*
* @return true
if a viewport should force the panel's width to match its own width, allowing this panel to manage its own horizontal scrolling
* and/or wrapping.
* @see #setTracksViewportHeight
*/
public boolean getScrollableTracksViewportWidth() {
if(getParent() instanceof JViewport) { //if the panel is inside a viewport
final JViewport viewport = (JViewport)getParent(); //cast the parent to a viewport
if(tracksViewportWidth) { //if we track our own width
return viewport.getWidth() >= getMinimumSize().width; //make sure we can be as small as the viewport wants us to be
} else { //if we don't track our own width, fill the viewport if we're not big enough
return getPreferredSize().width < viewport.getWidth(); //if we're smaller than the viewport, let the viewport size us to fill the viewport
}
} else { //if we're not in a viewport
return tracksViewportWidth; //use whatever value we were assigned
}
}
/** Whether a viewport should always force the height of this panel to match its own. */
private boolean tracksViewportHeight = false;
/**
* Sets whether a viewport should always force the height of this panel to match its own height, unless the panel at its smallest could not fit inside the
* viewport
* @param verticallySelfManaged true
if the panel should not be scrolled vertically by any parent viewport, allowing this panel to
* manage its own vertical scrolling and/or wrapping.
*/
public void setTracksViewportHeight(final boolean verticallySelfManaged) {
tracksViewportHeight = verticallySelfManaged;
}
/**
* Returns whether a viewport should always force the height of this panel to match the height of the viewport.
*
* This implementation defaults to false
, allowing the viewport to scroll vertically as needed.
*
* @return true
if a viewport should force the panel's height to match its own height, allowing this panel to manage its own vertical scrolling
* and/or wrapping.
* @see #setTracksViewportHeight
*/
public boolean getScrollableTracksViewportHeight() {
if(getParent() instanceof JViewport) { //if the panel is inside a viewport
final JViewport viewport = (JViewport)getParent(); //cast the parent to a viewport
if(tracksViewportHeight) { //if we track our own height
return viewport.getHeight() >= getMinimumSize().height; //make sure we can be as small as the viewport wants us to be
} else { //if we don't track our own height, fill the viewport if we're not big enough
return getPreferredSize().height < viewport.getHeight(); //if we're smaller than the viewport, let the viewport size us to fill the viewport
}
} else { //if we're not in a viewport
return tracksViewportHeight; //use whatever value we were assigned
}
}
}