/*
* AP(r) Computer Science GridWorld Case Study:
* Copyright(c) 2002-2006 College Entrance Examination Board
* (http://www.collegeboard.com).
*
* This code 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.
*
* This code 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.
*
* @author Julie Zelenski
* @author Cay Horstmann
*/
package info.gridworld.gui;
import info.gridworld.grid.Grid;
import info.gridworld.grid.Location;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.ResourceBundle;
import javax.swing.JPanel;
import javax.swing.JToolTip;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
/**
* A <code>GridPanel</code> is a panel containing a graphical display of the
* grid occupants. <br />
* This code is not tested on the AP CS A and AB exams. It contains GUI
* implementation details that are not intended to be understood by AP CS
* students.
*/
public class GridPanel extends JPanel implements Scrollable,
PseudoInfiniteViewport.Pannable
{
private static final int MIN_CELL_SIZE = 12;
private static final int DEFAULT_CELL_SIZE = 48;
private static final int DEFAULT_CELL_COUNT = 10;
private static final int TIP_DELAY = 1000;
private Grid<?> grid;
private int numRows, numCols, originRow, originCol;
private int cellSize; // the size of each cell, EXCLUDING the gridlines
private boolean toolTipsEnabled;
private Color backgroundColor = Color.WHITE;
private ResourceBundle resources;
private DisplayMap displayMap;
private Location currentLocation;
private Timer tipTimer;
private JToolTip tip;
private JPanel glassPane;
/**
* Construct a new GridPanel object with no grid. The view will be
* empty.
*/
public GridPanel(DisplayMap map, ResourceBundle res)
{
displayMap = map;
resources = res;
setToolTipsEnabled(true);
}
/**
* Paint this component.
* @param g the Graphics object to use to render this component
*/
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D) g;
super.paintComponent(g2);
if (grid == null)
return;
Insets insets = getInsets();
g2.setColor(backgroundColor);
g2.fillRect(insets.left, insets.top, numCols * (cellSize + 1) + 1, numRows
* (cellSize + 1) + 1);
drawWatermark(g2);
drawGridlines(g2);
drawOccupants(g2);
drawCurrentLocation(g2);
}
/**
* Draw one occupant object. First verify that the object is actually
* visible before any drawing, set up the clip appropriately and use the
* DisplayMap to determine which object to call upon to render this
* particular Locatable. Note that we save and restore the graphics
* transform to restore back to normalcy no matter what the renderer did to
* to the coordinate system.
* @param g2 the Graphics2D object to use to render
* @param xleft the leftmost pixel of the rectangle
* @param ytop the topmost pixel of the rectangle
* @param obj the Locatable object to draw
*/
private void drawOccupant(Graphics2D g2, int xleft, int ytop, Object obj)
{
Rectangle cellToDraw = new Rectangle(xleft, ytop, cellSize, cellSize);
// Only draw if the object is visible within the current clipping
// region.
if (cellToDraw.intersects(g2.getClip().getBounds()))
{
Graphics2D g2copy = (Graphics2D) g2.create();
g2copy.clip(cellToDraw);
// Get the drawing object to display this occupant.
Display displayObj = displayMap.findDisplayFor(obj.getClass());
displayObj.draw(obj, this, g2copy, cellToDraw);
g2copy.dispose();
}
}
/**
* Draw the gridlines for the grid. We only draw the portion of the
* lines that intersect the current clipping bounds.
* @param g2 the Graphics2 object to use to render
*/
private void drawGridlines(Graphics2D g2)
{
Rectangle curClip = g2.getClip().getBounds();
int top = getInsets().top, left = getInsets().left;
int miny = Math.max(0, (curClip.y - top) / (cellSize + 1)) * (cellSize + 1) + top;
int minx = Math.max(0, (curClip.x - left) / (cellSize + 1)) * (cellSize + 1) + left;
int maxy = Math.min(numRows,
(curClip.y + curClip.height - top + cellSize) / (cellSize + 1))
* (cellSize + 1) + top;
int maxx = Math.min(numCols,
(curClip.x + curClip.width - left + cellSize) / (cellSize + 1))
* (cellSize + 1) + left;
g2.setColor(Color.GRAY);
for (int y = miny; y <= maxy; y += cellSize + 1)
for (int x = minx; x <= maxx; x += cellSize + 1)
{
Location loc = locationForPoint(
new Point(x + cellSize / 2, y + cellSize / 2));
if (loc != null && !grid.isValid(loc))
g2.fillRect(x + 1, y + 1, cellSize, cellSize);
}
g2.setColor(Color.BLACK);
for (int y = miny; y <= maxy; y += cellSize + 1)
// draw horizontal lines
g2.drawLine(minx, y, maxx, y);
for (int x = minx; x <= maxx; x += cellSize + 1)
// draw vertical lines
g2.drawLine(x, miny, x, maxy);
}
/**
* Draws the occupants of the grid.
* @param g2 the graphics context
*/
private void drawOccupants(Graphics2D g2)
{
ArrayList<Location> occupantLocs = grid.getOccupiedLocations();
for (int index = 0; index < occupantLocs.size(); index++)
{
Location loc = (Location) occupantLocs.get(index);
int xleft = colToXCoord(loc.getCol());
int ytop = rowToYCoord(loc.getRow());
drawOccupant(g2, xleft, ytop, grid.get(loc));
}
}
/**
* Draws a square that marks the current location.
* @param g2 the graphics context
*/
private void drawCurrentLocation(Graphics2D g2)
{
if ("hide".equals(System.getProperty("info.gridworld.gui.selection")))
return;
if (currentLocation != null)
{
Point p = pointForLocation(currentLocation);
g2.drawRect(p.x - cellSize / 2 - 2, p.y - cellSize / 2 - 2,
cellSize + 3, cellSize + 3);
}
}
/**
* Draws a watermark that shows the version number if it is < 1.0
* @param g2 the graphics context
*/
private void drawWatermark(Graphics2D g2)
{
if ("hide".equals(System.getProperty("info.gridworld.gui.watermark")))
return;
g2 = (Graphics2D) g2.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Rectangle rect = getBounds();
g2.setPaint(new Color(0xE3, 0xD3, 0xD3));
final int WATERMARK_FONT_SIZE = 100;
String s = resources.getString("version.id");
if ("1.0".compareTo(s) <= 0) return;
g2.setFont(new Font("SansSerif", Font.BOLD, WATERMARK_FONT_SIZE));
FontRenderContext frc = g2.getFontRenderContext();
Rectangle2D bounds = g2.getFont().getStringBounds(s, frc);
float centerX = rect.x + rect.width / 2;
float centerY = rect.y + rect.height / 2;
float leftX = centerX - (float) bounds.getWidth() / 2;
LineMetrics lm = g2.getFont().getLineMetrics(s, frc);
float baselineY = centerY - lm.getHeight() / 2 + lm.getAscent();
g2.drawString(s, leftX, baselineY);
}
/**
* Enables/disables showing of tooltip giving information about the
* occupant beneath the mouse.
* @param flag true/false to enable/disable tool tips
*/
public void setToolTipsEnabled(boolean flag)
{
if ("hide".equals(System.getProperty("info.gridworld.gui.tooltips")))
flag = false;
if (flag)
ToolTipManager.sharedInstance().registerComponent(this);
else
ToolTipManager.sharedInstance().unregisterComponent(this);
toolTipsEnabled = flag;
}
/**
* Sets the grid being displayed. Reset the cellSize to be the
* largest that fits the entire grid in the current visible area (use
* default if grid is too large).
* @param gr the grid to display
*/
public void setGrid(Grid<?> gr)
{
currentLocation = new Location(0, 0);
JViewport vp = getEnclosingViewport(); // before changing, reset
// scroll/pan position
if (vp != null)
vp.setViewPosition(new Point(0, 0));
grid = gr;
originRow = originCol = 0;
if (grid.getNumRows() == -1 && grid.getNumCols() == -1)
{
numRows = numCols = 2000;
// This determines the "virtual" size of the pan world
}
else
{
numRows = grid.getNumRows();
numCols = grid.getNumCols();
}
recalculateCellSize(MIN_CELL_SIZE);
}
// private helpers to calculate extra width/height needs for borders/insets.
private int extraWidth()
{
return getInsets().left + getInsets().right;
}
private int extraHeight()
{
return getInsets().top + getInsets().left;
}
/**
* Returns the desired size of the display, for use by layout manager.
* @return preferred size
*/
public Dimension getPreferredSize()
{
return new Dimension(numCols * (cellSize + 1) + 1 + extraWidth(),
numRows * (cellSize + 1) + 1 + extraHeight());
}
/**
* Returns the minimum size of the display, for use by layout manager.
* @return minimum size
*/
public Dimension getMinimumSize()
{
return new Dimension(numCols * (MIN_CELL_SIZE + 1) + 1 + extraWidth(),
numRows * (MIN_CELL_SIZE + 1) + 1 + extraHeight());
}
/**
* Zooms in the display by doubling the current cell size.
*/
public void zoomIn()
{
cellSize *= 2;
revalidate();
}
/**
* Zooms out the display by halving the current cell size.
*/
public void zoomOut()
{
cellSize = Math.max(cellSize / 2, MIN_CELL_SIZE);
revalidate();
}
/**
* Pans the display back to the origin, so that 0, 0 is at the the upper
* left of the visible viewport.
*/
public void recenter(Location loc)
{
originRow = loc.getRow();
originCol = loc.getCol();
repaint();
JViewport vp = getEnclosingViewport();
if (vp != null)
{
if (!isPannableUnbounded()
|| !(vp instanceof PseudoInfiniteViewport))
vp.setViewPosition(pointForLocation(loc));
else
showPanTip();
}
}
/**
* Given a Point determine which grid location (if any) is under the
* mouse. This method is used by the GUI when creating Fish by clicking on
* cells in the display.
* @param p the Point in question (in display's coordinate system)
* @return the Location beneath the event (which may not be a
* valid location in the grid)
*/
public Location locationForPoint(Point p)
{
return new Location(yCoordToRow(p.y), xCoordToCol(p.x));
}
public Point pointForLocation(Location loc)
{
return new Point(colToXCoord(loc.getCol()) + cellSize / 2,
rowToYCoord(loc.getRow()) + cellSize / 2);
}
// private helpers to convert between (x,y) and (row,col)
private int xCoordToCol(int xCoord)
{
return (xCoord - 1 - getInsets().left) / (cellSize + 1) + originCol;
}
private int yCoordToRow(int yCoord)
{
return (yCoord - 1 - getInsets().top) / (cellSize + 1) + originRow;
}
private int colToXCoord(int col)
{
return (col - originCol) * (cellSize + 1) + 1 + getInsets().left;
}
private int rowToYCoord(int row)
{
return (row - originRow) * (cellSize + 1) + 1 + getInsets().top;
}
/**
* Given a MouseEvent, determine what text to place in the floating tool tip
* when the the mouse hovers over this location. If the mouse is over a
* valid grid cell. we provide some information about the cell and
* its contents. This method is automatically called on mouse-moved events
* since we register for tool tips.
* @param evt the MouseEvent in question
* @return the tool tip string for this location
*/
public String getToolTipText(MouseEvent evt)
{
Location loc = locationForPoint(evt.getPoint());
return getToolTipText(loc);
}
private String getToolTipText(Location loc)
{
if (!toolTipsEnabled || loc == null || !grid.isValid(loc))
return null;
Object f = grid.get(loc);
if (f != null)
return MessageFormat.format(resources
.getString("cell.tooltip.nonempty"), new Object[]
{ loc, f });
else
return MessageFormat.format(resources
.getString("cell.tooltip.empty"), new Object[]
{ loc, f });
}
/**
* Sets the current location.
* @param loc the new location
*/
public void setCurrentLocation(Location loc)
{
currentLocation = loc;
}
/**
* Gets the current location.
* @return the currently selected location (marked with a bold square)
*/
public Location getCurrentLocation()
{
return currentLocation;
}
/**
* Moves the current location by a given amount.
* @param dr the number of rows by which to move the location
* @param dc the number of columns by which to move the location
*/
public void moveLocation(int dr, int dc)
{
Location newLocation = new Location(currentLocation.getRow() + dr,
currentLocation.getCol() + dc);
if (!grid.isValid(newLocation))
return;
currentLocation = newLocation;
JViewport viewPort = getEnclosingViewport();
if (isPannableUnbounded())
{
if (originRow > currentLocation.getRow())
originRow = currentLocation.getRow();
if (originCol > currentLocation.getCol())
originCol = currentLocation.getCol();
Dimension dim = viewPort.getSize();
int rows = dim.height / (cellSize + 1);
int cols = dim.width / (cellSize + 1);
if (originRow + rows - 1 < currentLocation.getRow())
originRow = currentLocation.getRow() - rows + 1;
if (originCol + rows - 1 < currentLocation.getCol())
originCol = currentLocation.getCol() - cols + 1;
}
else if (viewPort != null)
{
int dx = 0;
int dy = 0;
Point p = pointForLocation(currentLocation);
Rectangle locRect = new Rectangle(p.x - cellSize / 2, p.y
- cellSize / 2, cellSize + 1, cellSize + 1);
Rectangle viewRect = viewPort.getViewRect();
if (!viewRect.contains(locRect))
{
while (locRect.x < viewRect.x + dx)
dx -= cellSize + 1;
while (locRect.y < viewRect.y + dy)
dy -= cellSize + 1;
while (locRect.getMaxX() > viewRect.getMaxX() + dx)
dx += cellSize + 1;
while (locRect.getMaxY() > viewRect.getMaxY() + dy)
dy += cellSize + 1;
Point pt = viewPort.getViewPosition();
pt.x += dx;
pt.y += dy;
viewPort.setViewPosition(pt);
}
}
repaint();
showTip(getToolTipText(currentLocation),
pointForLocation(currentLocation));
}
/**
* Show a tool tip.
* @param tipText the tool tip text
* @param pt the pixel position over which to show the tip
*/
public void showTip(String tipText, Point pt)
{
if (getRootPane() == null)
return;
// draw in glass pane to appear on top of other components
if (glassPane == null)
{
getRootPane().setGlassPane(glassPane = new JPanel());
glassPane.setOpaque(false);
glassPane.setLayout(null); // will control layout manually
glassPane.add(tip = new JToolTip());
tipTimer = new Timer(TIP_DELAY, new ActionListener()
{
public void actionPerformed(ActionEvent evt)
{
glassPane.setVisible(false);
}
});
tipTimer.setRepeats(false);
}
if (tipText == null)
return;
// set tip text to identify current origin of pannable view
tip.setTipText(tipText);
// position tip to appear at upper left corner of viewport
tip.setLocation(SwingUtilities.convertPoint(this, pt, glassPane));
tip.setSize(tip.getPreferredSize());
// show glass pane (it contains tip)
glassPane.setVisible(true);
glassPane.repaint();
// this timer will hide the glass pane after a short delay
tipTimer.restart();
}
/**
* Calculate the cell size to use given the current viewable region and the
* the number of rows and columns in the grid. We use the largest
* cellSize that will fit in the viewable region, bounded to be at least the
* parameter minSize.
*/
private void recalculateCellSize(int minSize)
{
if (numRows == 0 || numCols == 0)
{
cellSize = 0;
}
else
{
JViewport vp = getEnclosingViewport();
Dimension viewableSize = (vp != null) ? vp.getSize() : getSize();
int desiredCellSize = Math.min(
(viewableSize.height - extraHeight()) / numRows,
(viewableSize.width - extraWidth()) / numCols) - 1;
// now we want to approximate this with
// DEFAULT_CELL_SIZE * Math.pow(2, k)
cellSize = DEFAULT_CELL_SIZE;
if (cellSize <= desiredCellSize)
while (2 * cellSize <= desiredCellSize)
cellSize *= 2;
else
while (cellSize / 2 >= Math.max(desiredCellSize, MIN_CELL_SIZE))
cellSize /= 2;
}
revalidate();
}
// helper to get our parent viewport, if we are in one.
private JViewport getEnclosingViewport()
{
Component parent = getParent();
return (parent instanceof JViewport) ? (JViewport) parent : null;
}
// GridPanel implements the Scrollable interface to get nicer behavior in a
// JScrollPane. The 5 methods below are the methods in that interface
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation, int direction)
{
return cellSize + 1;
}
public int getScrollableBlockIncrement(Rectangle visibleRect,
int orientation, int direction)
{
if (orientation == SwingConstants.VERTICAL)
return (int) (visibleRect.height * .9);
else
return (int) (visibleRect.width * .9);
}
public boolean getScrollableTracksViewportWidth()
{
return false;
}
public boolean getScrollableTracksViewportHeight()
{
return false;
}
public Dimension getPreferredScrollableViewportSize()
{
return new Dimension(DEFAULT_CELL_COUNT * (DEFAULT_CELL_SIZE + 1) + 1 + extraWidth(),
DEFAULT_CELL_COUNT * (DEFAULT_CELL_SIZE + 1) + 1 + extraHeight());
}
// GridPanel implements the PseudoInfiniteViewport.Pannable interface to
// play nicely with the pan behavior for unbounded view.
// The 3 methods below are the methods in that interface.
public void panBy(int hDelta, int vDelta)
{
originCol += hDelta / (cellSize + 1);
originRow += vDelta / (cellSize + 1);
repaint();
}
public boolean isPannableUnbounded()
{
return grid != null && (grid.getNumRows() == -1 || grid.getNumCols() == -1);
}
/**
* Shows a tool tip over the upper left corner of the viewport with the
* contents of the pannable view's pannable tip text (typically a string
* identifiying the corner point). Tip is removed after a short delay.
*/
public void showPanTip()
{
String tipText = null;
Point upperLeft = new Point(0, 0);
JViewport vp = getEnclosingViewport();
if (!isPannableUnbounded() && vp != null)
upperLeft = vp.getViewPosition();
Location loc = locationForPoint(upperLeft);
if (loc != null)
tipText = getToolTipText(loc);
showTip(tipText, getLocation());
}
}