Newer
Older
piccolo2d.java / extras / src / main / java / edu / umd / cs / piccolox / swing / PScrollPane.java
@Allain Lalonde Allain Lalonde on 31 Oct 2009 14 KB Fixed Issue 131
/*
 * Copyright (c) 2008-2009, Piccolo2D project, http://piccolo2d.org
 * Copyright (c) 1998-2008, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
 * contributors may be used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.umd.cs.piccolox.swing;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.plaf.ScrollPaneUI;

import edu.umd.cs.piccolo.PCanvas;

/**
 * A simple extension to a standard scroll pane that uses the jazz version of
 * the viewport by default. Also uses the jazz version of ScrollPaneLayout
 * 
 * @author Lance Good
 */
public class PScrollPane extends JScrollPane {

	private static final long serialVersionUID = 1L;

	/** A reusable null action. */
	protected PNullAction nullAction = null;

	/** Controls whether key actions are disabled on this component. */
	protected boolean disableKeyActions = false;

	private final AdjustmentListener scrollAdjustmentListener = new AdjustmentListener() {
		private boolean lastAdjustingState = false;

		public void adjustmentValueChanged(AdjustmentEvent e) {
			if (e.getSource() instanceof JScrollBar) {
				JScrollBar scrollBar = (JScrollBar) e.getSource();
				
				setAdjusting(scrollBar.getValueIsAdjusting());
			}
		}

		/**
		 * Updates the underlying PCanvas' interacting flag depending on whether
		 * scroll bar adjustments are still taking place.
		 * 
		 * @param isAdjusting
		 *          true if the scroll bar is still being interacted with
		 */
		private void setAdjusting(final boolean isAdjusting) {
			if (isAdjusting != lastAdjustingState) {
				Component c = getViewport().getView();
				if (c instanceof PCanvas) {
					((PCanvas) c).setInteracting(isAdjusting);
				}
				lastAdjustingState = isAdjusting;
			}
		}
	};

	/**
	 * Constructs a scollpane for the provided component with the specified
	 * scrollbar policies.
	 * 
	 * @param view
	 *          component being viewed through the scrollpane
	 * @param vsbPolicy
	 *          vertical scroll bar policy
	 * @param hsbPolicy
	 *          horizontal scroll bar policy
	 */
	public PScrollPane(final Component view, final int vsbPolicy, final int hsbPolicy) {
		super(view, vsbPolicy, hsbPolicy);

		// Set the layout and sync it with the scroll pane
		final PScrollPaneLayout layout = new PScrollPaneLayout.UIResource();
		setLayout(layout);
		layout.syncWithScrollPane(this);

		horizontalScrollBar.addAdjustmentListener(scrollAdjustmentListener);
		verticalScrollBar.addAdjustmentListener(scrollAdjustmentListener);
	}

	public void setVerticalScrollBar(javax.swing.JScrollBar newVerticalScrollBar) {
		if (verticalScrollBar != null) {
			verticalScrollBar.removeAdjustmentListener(scrollAdjustmentListener);
		}

		super.setVerticalScrollBar(newVerticalScrollBar);
		newVerticalScrollBar.addAdjustmentListener(scrollAdjustmentListener);
	}

	public void setHorizontalScrollBar(javax.swing.JScrollBar newHorizontalScrollBar) {
		if (horizontalScrollBar != null) {
			horizontalScrollBar.removeAdjustmentListener(scrollAdjustmentListener);
		}
		
		super.setHorizontalScrollBar(newHorizontalScrollBar);
		newHorizontalScrollBar.addAdjustmentListener(scrollAdjustmentListener);
	}

	/**
	 * Constructs a scollpane for the provided component.
	 * 
	 * @param view
	 *          component being viewed through the scrollpane
	 */
	public PScrollPane(final Component view) {
		this(view, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED);
	}

	/**
	 * Constructs a scollpane not attached to any component with the specified
	 * scroll bar policies.
	 * 
	 * @param vsbPolicy
	 *          vertical scroll bar policy
	 * @param hsbPolicy
	 *          horizontal scroll bar policy
	 */
	public PScrollPane(final int vsbPolicy, final int hsbPolicy) {
		this(null, vsbPolicy, hsbPolicy);
	}

	/**
	 * Constructs a scollpane not attached to any component.
	 */
	public PScrollPane() {
		this(null, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED);
	}

	/**
	 * Disable or enable key actions on this PScrollPane.
	 * 
	 * @param disable
	 *          true disables key actions, false enables key actions
	 */
	public void setKeyActionsDisabled(final boolean disable) {
		if (disable && disableKeyActions != disable) {
			disableKeyActions = disable;
			disableKeyActions();
		} else if (!disable && disableKeyActions != disable) {
			disableKeyActions = disable;
			installCustomKeyActions();
		}
	}

	/**
	 * Sets the UI.
	 * 
	 * @param ui
	 *          the scroll pane ui to associate with this PScollPane
	 */
	public void setUI(final ScrollPaneUI ui) {
		super.setUI(ui);

		if (!disableKeyActions) {
			installCustomKeyActions();
		} else {
			disableKeyActions();
		}
	}

	/**
	 * Install custom key actions (in place of the Swing defaults) to correctly
	 * scroll the view.
	 */
	protected void installCustomKeyActions() {
		final ActionMap map = getActionMap();

		map.put("scrollUp", new PScrollAction("scrollUp", SwingConstants.VERTICAL, -1, true));
		map.put("scrollDown", new PScrollAction("scrollDown", SwingConstants.VERTICAL, 1,
				true));
		map.put("scrollLeft", new PScrollAction("scrollLeft", SwingConstants.HORIZONTAL, -1,
				true));

		map.put("scrollRight", new PScrollAction("ScrollRight", SwingConstants.HORIZONTAL, 1,
				true));
		map.put("unitScrollRight", new PScrollAction("UnitScrollRight",
				SwingConstants.HORIZONTAL, 1, false));
		map.put("unitScrollLeft", new PScrollAction("UnitScrollLeft",
				SwingConstants.HORIZONTAL, -1, false));
		map.put("unitScrollUp", new PScrollAction("UnitScrollUp", SwingConstants.VERTICAL,
				-1, false));
		map.put("unitScrollDown", new PScrollAction("UnitScrollDown",
				SwingConstants.VERTICAL, 1, false));

		map.put("scrollEnd", new PScrollEndAction("ScrollEnd"));
		map.put("scrollHome", new PScrollHomeAction("ScrollHome"));
	}

	/**
	 * Disables key actions on this PScrollPane.
	 */
	protected void disableKeyActions() {
		final ActionMap map = getActionMap();

		if (nullAction == null) {
			nullAction = new PNullAction();
		}

		map.put("scrollUp", nullAction);
		map.put("scrollDown", nullAction);
		map.put("scrollLeft", nullAction);
		map.put("scrollRight", nullAction);
		map.put("unitScrollRight", nullAction);
		map.put("unitScrollLeft", nullAction);
		map.put("unitScrollUp", nullAction);
		map.put("unitScrollDown", nullAction);
		map.put("scrollEnd", nullAction);
		map.put("scrollHome", nullAction);
	}

	/**
	 * Overridden to create the Piccolo2D viewport.
	 * 
	 * @return the Piccolo2D version of the viewport
	 */
	protected JViewport createViewport() {
		return new PViewport();
	}

	/**
	 * Action to scroll left/right/up/down. Modified from
	 * javax.swing.plaf.basic.BasicScrollPaneUI.ScrollAction.
	 * 
	 * Gets the view parameters (position and size) from the Viewport rather than
	 * directly from the view - also only performs its actions when the relevant
	 * scrollbar is visible.
	 */
	protected static class PScrollAction extends AbstractAction {
		private static final int MINIMUM_SCROLL_SIZE = 10;
		private static final long serialVersionUID = 1L;
		/** Direction to scroll. */
		protected int orientation;
		/** 1 indicates scroll down, -1 up. */
		protected int direction;
		/** True indicates a block scroll, otherwise a unit scroll. */
		private final boolean block;

		/**
		 * Constructs a scroll action with the given name in the given orientiation
		 * stated and in the direction provided.
		 * 
		 * @param name
		 *          arbitrary name of action
		 * @param orientation
		 *          horizontal or vertical
		 * @param direction
		 *          1 indicates scroll down, -1 up
		 * @param block
		 *          true if block scroll as opposed to unit
		 */
		protected PScrollAction(final String name, final int orientation,
				final int direction, final boolean block) {
			super(name);
			this.orientation = orientation;
			this.direction = direction;
			this.block = block;
		}

		/**
		 * Performs the scroll action if the action was performed on visible
		 * scrollbars and if the viewport is valid.
		 * 
		 * @param event
		 *          the event responsible for this action being performed
		 */
		public void actionPerformed(final ActionEvent event) {
			final JScrollPane scrollpane = (JScrollPane) event.getSource();
			if (!isScrollEventOnVisibleScrollbars(scrollpane)) {
				return;
			}

			final JViewport vp = scrollpane.getViewport();
			if (vp == null) {
				return;
			}

			Component view = vp.getView();
			if (view == null) {
				return;
			}

			final Rectangle visRect = vp.getViewRect();
			// LEG: Modification to query the viewport for the
			// view size rather than going directly to the view
			final Dimension vSize = vp.getViewSize();
			final int amount;

			if (view instanceof Scrollable) {
				if (block) {
					amount = ((Scrollable) view).getScrollableBlockIncrement(visRect, orientation,
						direction);
				} else {
					amount = ((Scrollable) view).getScrollableUnitIncrement(visRect, orientation,
						direction);
				}
			} else {
				if (block) {
					if (orientation == SwingConstants.VERTICAL) {
						amount = visRect.height;
					} else {
						amount = visRect.width;
					}
				} else {
					amount = MINIMUM_SCROLL_SIZE;
				}
			}

			if (orientation == SwingConstants.VERTICAL) {
				visRect.y += amount * direction;
				if (visRect.y + visRect.height > vSize.height) {
					visRect.y = Math.max(0, vSize.height - visRect.height);
				} else if (visRect.y < 0) {
					visRect.y = 0;
				}
			} else {
				visRect.x += amount * direction;
				if (visRect.x + visRect.width > vSize.width) {
					visRect.x = Math.max(0, vSize.width - visRect.width);
				} else if (visRect.x < 0) {
					visRect.x = 0;
				}
			}
			vp.setViewPosition(visRect.getLocation());
		}

		private boolean isScrollEventOnVisibleScrollbars(final JScrollPane scrollpane) {
			return orientation == SwingConstants.VERTICAL
					&& scrollpane.getVerticalScrollBar().isShowing()
					|| orientation == SwingConstants.HORIZONTAL
					&& scrollpane.getHorizontalScrollBar().isShowing();
		}
	}

	/**
	 * Action to scroll to x,y location of 0,0. Modified from
	 * javax.swing.plaf.basic.BasicScrollPaneUI.ScrollEndAction.
	 * 
	 * Only performs the event if a scrollbar is visible.
	 */
	private static class PScrollHomeAction extends AbstractAction {
		private static final long serialVersionUID = 1L;

		protected PScrollHomeAction(final String name) {
			super(name);
		}

		public void actionPerformed(final ActionEvent e) {
			final JScrollPane scrollpane = (JScrollPane) e.getSource();
			// LEG: Modification to only perform these actions if one of the
			// scrollbars is actually showing
			if (scrollpane.getVerticalScrollBar().isShowing()
					|| scrollpane.getHorizontalScrollBar().isShowing()) {
				final JViewport vp = scrollpane.getViewport();
				if (vp != null && vp.getView() != null) {
					vp.setViewPosition(new Point(0, 0));
				}
			}
		}
	}

	/**
	 * Action to scroll to last visible location. Modified from
	 * javax.swing.plaf.basic.BasicScrollPaneUI.ScrollEndAction.
	 * 
	 * Gets the view size from the viewport rather than directly from the view -
	 * also only performs the event if a scrollbar is visible.
	 */
	protected static class PScrollEndAction extends AbstractAction {
		private static final long serialVersionUID = 1L;

		/**
		 * Constructs a scroll to end action with the given name.
		 * 
		 * @param name
		 *          name to assign to this action
		 */
		protected PScrollEndAction(final String name) {
			super(name);
		}

		/**
		 * Scrolls to the end of the viewport if there are visible scrollbars.
		 * 
		 * @param event
		 *          event responsible for the scroll event
		 */
		public void actionPerformed(final ActionEvent event) {
			final JScrollPane scrollpane = (JScrollPane) event.getSource();
			// LEG: Modification to only perform these actions if one of the
			// scrollbars is actually showing
			if (scrollpane.getVerticalScrollBar().isShowing()
					|| scrollpane.getHorizontalScrollBar().isShowing()) {

				final JViewport vp = scrollpane.getViewport();
				if (vp != null && vp.getView() != null) {

					final Rectangle visRect = vp.getViewRect();
					// LEG: Modification to query the viewport for the
					// view size rather than going directly to the view
					final Dimension size = vp.getViewSize();
					vp.setViewPosition(new Point(size.width - visRect.width, size.height
							- visRect.height));
				}
			}
		}
	}

	/**
	 * An action to do nothing - put into an action map to keep it from looking to
	 * its parent.
	 */
	protected static class PNullAction extends AbstractAction {
		private static final long serialVersionUID = 1L;

		/**
		 * Does nothing.
		 * 
		 * @param e
		 *          Event responsible for this action
		 */
		public void actionPerformed(final ActionEvent e) {
		}
	}
}