// GuiBoard.java
package net.sf.gogui.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import javax.swing.JPanel;
import net.sf.gogui.boardpainter.BoardPainter;
import net.sf.gogui.boardpainter.ConstField;
import net.sf.gogui.boardpainter.Field;
import net.sf.gogui.go.BoardConstants;
import net.sf.gogui.go.GoColor;
import static net.sf.gogui.go.GoColor.EMPTY;
import net.sf.gogui.go.GoPoint;
import net.sf.gogui.util.ObjectUtil;
/** Graphical display of a Go board.
This class does not use go.Board, so it can be used with other board
implementations. It uses go.GoPoint for coordinates. */
public final class GuiBoard
extends JPanel
implements ConstGuiBoard, Printable
{
/** Callback for clicks on a field. */
public interface Listener
{
/** Callback for click on a field.
This callback is triggered with mouse clicks or the enter key
if the cursor is shown.
@param p The point clicked.
@param modifiedSelect Modified select. True if the click was a
double click or with the right mouse button or if a modifier key
(Ctrl, Alt, Meta) was pressed while clicking, as long as it was
not a (platform-dependent) popup menu trigger. */
void fieldClicked(GoPoint p, boolean modifiedSelect);
/** Callback for context menu.
This callback is triggered with mouse clicks that trigger
popup menus (platform-dependent).
@param point The point clicked.
@param invoker The awt.Component that was clicked on.
@param x The x coordinate on the invoker component.
@param y The y coordinate on the invoker component. */
void contextMenu(GoPoint point, Component invoker, int x, int y);
}
/** Constructor.
@param size The board size. */
public GuiBoard(int size)
{
m_painter = new BoardPainter();
setPreferredFieldSize();
initSize(size);
}
/** Clear every kind of markup. */
public void clearAll()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
setFieldBackground(GoPoint.get(x, y), null);
clearAllCrossHair();
clearAllMarkup();
clearAllSelect();
clearAllInfluence();
clearAllLabels();
clearAllGhostStones();
clearAllTerritory();
clearLastMove();
}
/** Clear all crosshairs. */
public void clearAllCrossHair()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
setCrossHair(GoPoint.get(x, y), false);
}
public void clearAllInfluence()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
clearInfluence(GoPoint.get(x, y));
}
/** Clear all markup.
Clears mark, circle, square, triangle on all points. */
public void clearAllMarkup()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
{
GoPoint point = GoPoint.get(x, y);
setMark(point, false);
setMarkCircle(point, false);
setMarkSquare(point, false);
setMarkTriangle(point, false);
}
}
/** Clear all selected points. */
public void clearAllSelect()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
setSelect(GoPoint.get(x, y), false);
}
/** Clear all labels. */
public void clearAllLabels()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
setLabel(GoPoint.get(x, y), "");
}
/** Clear all shadow stones. */
public void clearAllGhostStones()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
setGhostStone(GoPoint.get(x, y), null);
}
/** Clear all territory. */
public void clearAllTerritory()
{
for (int x = 0; x < m_size; ++x)
for (int y = 0; y < m_size; ++y)
setTerritory(GoPoint.get(x, y), EMPTY);
}
/** Clear influence. */
public void clearInfluence(GoPoint point)
{
getField(point).clearInfluence();
repaint(point);
}
/** Trigger the context menu callback at the listener. */
public void contextMenu(GoPoint point)
{
m_panel.contextMenu(point);
}
/** Get current board size. */
public int getBoardSize()
{
return m_size;
}
/** Return a field.
Returns only a const interface to the field, the field state should
be modified using GuiBoard functions to guarantee the UI repaint after
field changes. */
public ConstField getFieldConst(GoPoint p)
{
return getField(p);
}
public Dimension getFieldSize()
{
int size = m_painter.getFieldSize();
return new Dimension(size, size);
}
/** Get label.
@param point The point.
@return Label or null if point has no label. */
public String getLabel(GoPoint point)
{
return getField(point).getLabel();
}
/** Get location on screen for a point.
@param point The point.
@return Location on screen of center of point. */
public Point getLocationOnScreen(GoPoint point)
{
Point center = m_painter.getCenter(point.getX(), point.getY());
Point location = m_panel.getLocationOnScreen();
location.x += center.x;
location.y += center.y;
return location;
}
/** Check if point is marked.
This unspecified mark uses a diagonal cross.
@param point The point.
@return true, if point is marked. */
public boolean getMark(GoPoint point)
{
return getField(point).getMark();
}
/** Check if point is marked with a circle.
@param point The point.
@return true, if point is marked with a circle. */
public boolean getMarkCircle(GoPoint point)
{
return getField(point).getMarkCircle();
}
/** Check if point is marked with a square.
@param point The point.
@return true, if point is marked with a square. */
public boolean getMarkSquare(GoPoint point)
{
return getField(point).getMarkSquare();
}
/** Check if point is marked with a triangle.
@param point The point.
@return true, if point is marked with a triangle. */
public boolean getMarkTriangle(GoPoint point)
{
return getField(point).getMarkTriangle();
}
public Dimension getMinimumFieldSize()
{
return m_minimumFieldSize;
}
public Dimension getPreferredFieldSize()
{
return m_preferredFieldSize;
}
/** Check if point is selected.
@param point The point.
@return true, if point is selected. */
public boolean getSelect(GoPoint point)
{
return getField(point).getSelect();
}
/** Check if cursor is shown.
@return true, if cursor is shown. */
public boolean getShowCursor()
{
return m_showCursor;
}
public boolean getShowGrid()
{
return m_showGrid;
}
/** Change the board size.
@param size The new board size. */
public void initSize(int size)
{
assert size > 0 && size <= GoPoint.MAX_SIZE;
m_size = size;
m_constants = BoardConstants.get(size);
m_field = new Field[size][size];
removeAll();
m_cursor = null;
setLayout(new SquareLayout());
m_panel = new BoardPanel();
m_panel.addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent event) {
if (getShowCursor())
setCursor(m_cursor, true);
}
public void focusLost(FocusEvent event) {
if (getShowCursor())
setCursor(m_cursor, false);
}
});
addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent event) {
m_panel.requestFocusInWindow();
}
});
add(m_panel);
m_panel.requestFocusInWindow();
m_panel.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent event) {
GuiBoard.this.keyPressed(event);
}
});
m_panel.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
GoPoint point = m_panel.getPoint(e);
if (point == null)
return;
// mousePressed and mouseReleased (platform dependency)
if (e.isPopupTrigger())
{
contextMenu(point);
return;
}
int button = e.getButton();
int count = e.getClickCount();
if (button != MouseEvent.BUTTON1)
return;
if (count == 2)
fieldClicked(point, true);
else
{
int modifiers = e.getModifiers();
int mask = (ActionEvent.CTRL_MASK
| ActionEvent.ALT_MASK
| ActionEvent.META_MASK);
boolean modifiedSelect = ((modifiers & mask) != 0);
fieldClicked(point, modifiedSelect);
}
}
public void mouseReleased(MouseEvent e) {
GoPoint point = m_panel.getPoint(e);
if (point == null)
return;
if (e.isPopupTrigger())
{
contextMenu(point);
return;
}
}
});
m_panel.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseMoved(MouseEvent e) {
m_panel.setToolTipText(null);
GoPoint point = m_panel.getPoint(e);
if (point == null)
return;
String label = getField(point).getLabel();
if (label != null && label.length() > 3)
m_panel.setToolTipText(label);
}
});
for (int y = size - 1; y >= 0; --y)
for (int x = 0; x < size; ++x)
m_field[x][y] = new Field();
m_lastMove = null;
setCursor(GoPoint.get(m_size / 2, m_size / 2));
revalidate();
m_dirty = new Rectangle(0, 0, getWidth(), getHeight());
repaint();
}
/** Mark point of last move on the board.
The last move marker will be removed, if the parameter is null. */
public void markLastMove(GoPoint point)
{
clearLastMove();
m_lastMove = point;
if (m_lastMove != null)
{
Field field = getField(m_lastMove);
field.setLastMoveMarker(true);
repaint(point);
m_lastMove = point;
}
}
public void paintImmediately(GoPoint point)
{
m_panel.paintImmediately(point);
}
public int print(Graphics g, PageFormat format, int page)
throws PrinterException
{
if (page >= 1)
{
return Printable.NO_SUCH_PAGE;
}
double width = getSize().width;
double height = getSize().height;
double pageWidth = format.getImageableWidth();
double pageHeight = format.getImageableHeight();
double scale = 1;
if (width >= pageWidth)
scale = pageWidth / width;
double xSpace = (pageWidth - width * scale) / 2;
double ySpace = (pageHeight - height * scale) / 2;
Graphics2D g2d = (Graphics2D)g;
g2d.translate(format.getImageableX() + xSpace,
format.getImageableY() + ySpace);
g2d.scale(scale, scale);
print(g2d);
return Printable.PAGE_EXISTS;
}
/** Set or remove stone.
@param point The point.
@param color The stone color or EMPTY to remove a stone,
if existing. */
public void setColor(GoPoint point, GoColor color)
{
Field field = getField(point);
if (field.getColor() != color)
{
field.setColor(color);
m_panel.repaintWithShadow(point);
}
}
/** Set the cursor.
@param point New location of the cursor. */
public void setCursor(GoPoint point)
{
if (point != null && ! point.isOnBoard(m_size))
point = null;
if (! GoPoint.equals(m_cursor, point))
{
setCursor(m_cursor, false);
if (getShowCursor())
setCursor(point, true);
m_cursor = point;
}
}
/** Set the field background color.
@param point The location of the field.
@param color The color. */
public void setFieldBackground(GoPoint point, Color color)
{
Field field = getField(point);
if ((field.getFieldBackground() == null && color != null)
|| (field.getFieldBackground() != null
&& ! field.getFieldBackground().equals(color)))
{
field.setFieldBackground(color);
repaint(point);
}
}
/** Set crosshair.
@param point The point.
@param crossHair True to set, false to remove crosshair. */
public void setCrossHair(GoPoint point, boolean crossHair)
{
Field field = getField(point);
if (field.getCrossHair() != crossHair)
{
field.setCrossHair(crossHair);
repaint(point);
}
}
public void setGhostStone(GoPoint point, GoColor color)
{
Field field = getField(point);
if (! ObjectUtil.equals(field.getGhostStone(), color))
{
field.setGhostStone(color);
m_panel.repaintWithShadow(point);
}
}
/** Set influence value.
@param point The point.
@param value The influence value between -1 and 1. */
public void setInfluence(GoPoint point, double value)
{
getField(point).setInfluence(value);
repaint(point);
}
/** Set label.
@param point The point.
@param label The label. Should not be longer than 3 characters to
avoid clipping. null to remove label. */
public void setLabel(GoPoint point, String label)
{
Field field = getField(point);
if ((field.getLabel() == null && label != null)
|| (field.getLabel() != null
&& ! field.getLabel().equals(label)))
{
field.setLabel(label);
repaint(point);
}
}
/** Set the listener.
@param listener The new listener; null to set no listener. */
public void setListener(Listener listener)
{
m_listener = listener;
}
/** Set markup.
This unspecified markup uses a diagonal cross.
@param point The point.
@param mark True to set, false to remove. */
public void setMark(GoPoint point, boolean mark)
{
Field field = getField(point);
if (field.getMark() != mark)
{
getField(point).setMark(mark);
repaint(point);
}
}
/** Set circle markup.
@param point The point.
@param mark True to set, false to remove. */
public void setMarkCircle(GoPoint point, boolean mark)
{
Field field = getField(point);
if (field.getMarkCircle() != mark)
{
getField(point).setMarkCircle(mark);
repaint(point);
}
}
/** Set square markup.
@param point The point.
@param mark True to set, false to remove. */
public void setMarkSquare(GoPoint point, boolean mark)
{
Field field = getField(point);
if (field.getMarkSquare() != mark)
{
getField(point).setMarkSquare(mark);
repaint(point);
}
}
/** Set triangle markup.
@param point The point.
@param mark True to set, false to remove. */
public void setMarkTriangle(GoPoint point, boolean mark)
{
Field field = getField(point);
if (field.getMarkTriangle() != mark)
{
getField(point).setMarkTriangle(mark);
repaint(point);
}
}
public void setPreferredFieldSize(Dimension size)
{
m_preferredFieldSize = size;
m_panel.setPreferredFieldSize();
}
/** Set point selection markup.
@param point The point.
@param select True to set, false to remove. */
public void setSelect(GoPoint point, boolean select)
{
Field field = getField(point);
if (field.getSelect() != select)
{
getField(point).setSelect(select);
repaint(point);
}
}
/** Enable or disable cursor.
@param showCursor true to enable cursor. */
public void setShowCursor(boolean showCursor)
{
setCursor(m_cursor, false);
m_showCursor = showCursor;
if (m_showCursor)
setCursor(m_cursor, true);
m_panel.requestFocusInWindow();
}
/** Enable or disable grid coordinates.
@param showGrid true to enable grid coordinates. */
public void setShowGrid(boolean showGrid)
{
if (showGrid != m_showGrid)
{
m_showGrid = showGrid;
m_dirty = new Rectangle(0, 0, getWidth(), getHeight());
repaint();
}
}
/** Set territory.
@param point The point.
@param color The territory color for this point; EMPTY for
no territory. */
public void setTerritory(GoPoint point, GoColor color)
{
Field field = getField(point);
if (field.getTerritory() != color)
{
field.setTerritory(color);
repaint(point);
}
}
private class BoardPanel
extends JPanel
{
public BoardPanel()
{
setPreferredFieldSize();
setFocusable(true);
setOpaque(true);
}
public void contextMenu(GoPoint point)
{
if (m_listener == null)
return;
Point center = m_painter.getCenter(point.getX(), point.getY());
m_listener.contextMenu(point, this, center.x, center.y);
}
public GoPoint getPoint(MouseEvent event)
{
return m_painter.getPoint(event.getPoint());
}
public void paintComponent(Graphics graphics)
{
if (DEBUG_REPAINT)
System.err.println("BoardPanel.paintComponent "
+ graphics.getClipBounds().x + " "
+ graphics.getClipBounds().y + " "
+ graphics.getClipBounds().width + " "
+ graphics.getClipBounds().height);
int width = getWidth();
int height = getHeight();
if (m_image == null || width != m_imageWidth
|| height != m_imageHeight)
{
if (DEBUG_REPAINT)
System.err.println("createImage " + width + " " + height);
m_image = createImage(width, height);
m_imageWidth = width;
m_imageHeight = height;
m_dirty = new Rectangle(0, 0, width, height);
}
drawImage();
graphics.drawImage(m_image, 0, 0, null);
}
public void paintImmediately(GoPoint point)
{
if (DEBUG_REPAINT)
System.err.println("paintImmediately " + point);
Point location = m_painter.getLocation(point.getX(), point.getY());
Rectangle dirty = new Rectangle();
dirty.x = location.x;
dirty.y = location.y;
int offset = m_painter.getShadowOffset()
- Field.getStoneMargin(m_painter.getFieldSize());
dirty.width = m_painter.getFieldSize() + offset;
dirty.height = m_painter.getFieldSize() + offset;
addDirty(dirty);
Rectangle oldDirty = m_dirty;
m_dirty = dirty;
paintImmediately(dirty);
m_dirty = oldDirty;
}
public void repaint(GoPoint point)
{
if (DEBUG_REPAINT)
System.err.println("repaint " + point);
Point location = m_painter.getLocation(point.getX(), point.getY());
Rectangle dirty = new Rectangle();
dirty.x = location.x;
dirty.y = location.y;
dirty.width = m_painter.getFieldSize();
dirty.height = m_painter.getFieldSize();
addDirty(dirty);
repaint(dirty);
}
public void repaintWithShadow(GoPoint point)
{
if (DEBUG_REPAINT)
System.err.println("repaintWithShadow " + point);
Point location = m_painter.getLocation(point.getX(), point.getY());
Rectangle dirty = new Rectangle();
dirty.x = location.x;
dirty.y = location.y;
int offset = m_painter.getShadowOffset()
- Field.getStoneMargin(m_painter.getFieldSize());
dirty.width = m_painter.getFieldSize() + offset;
dirty.height = m_painter.getFieldSize() + offset;
addDirty(dirty);
repaint(dirty);
}
public final void setPreferredFieldSize()
{
int preferredFieldSize = getPreferredFieldSize().width;
setPreferredSize(BoardPainter.getPreferredSize(preferredFieldSize,
m_size, m_showGrid));
int minimumSize = 4 * m_size + 2;
setMinimumSize(new Dimension(minimumSize, minimumSize));
}
}
private static final boolean DEBUG_REPAINT = false;
private boolean m_showCursor = true;
private boolean m_showGrid = true;
private int m_imageHeight;
private int m_imageWidth;
private int m_size;
private BoardConstants m_constants;
private BoardPanel m_panel;
private Dimension m_minimumFieldSize;
private Dimension m_preferredFieldSize;
private GoPoint m_cursor;
private GoPoint m_lastMove;
private final BoardPainter m_painter;
private Field m_field[][];
private Image m_image;
private Listener m_listener;
private Rectangle m_dirty = new Rectangle();
private void addDirty(Rectangle rectangle)
{
if (m_dirty == null)
m_dirty = rectangle;
else
m_dirty.add(rectangle);
}
private void clearLastMove()
{
if (m_lastMove != null)
{
Field field = getField(m_lastMove);
field.setLastMoveMarker(false);
repaint(m_lastMove);
m_lastMove = null;
}
}
private void drawImage()
{
if (m_image == null || m_dirty == null)
return;
if (DEBUG_REPAINT)
System.err.println("BoardPanel.drawImage " + m_dirty.x + " "
+ m_dirty.y + " " + m_dirty.width + " "
+ m_dirty.height);
Graphics graphics = m_image.getGraphics();
graphics.setClip(m_dirty);
m_painter.draw(graphics, m_field, m_imageWidth, m_showGrid);
m_dirty = null;
}
private void fieldClicked(GoPoint p, boolean modifiedSelect)
{
if (m_listener != null)
m_listener.fieldClicked(p, modifiedSelect);
}
private Field getField(GoPoint p)
{
assert p != null;
return m_field[p.getX()][p.getY()];
}
private boolean isHandicapLineOrEdge(int line)
{
return m_constants.isHandicapLine(line)
|| m_constants.isEdgeLine(line);
}
private void keyPressed(KeyEvent event)
{
int code = event.getKeyCode();
int modifiers = event.getModifiers();
if (code == KeyEvent.VK_ENTER)
{
int mask = (ActionEvent.CTRL_MASK
| ActionEvent.ALT_MASK
| ActionEvent.META_MASK);
boolean modifiedSelect = ((modifiers & mask) != 0);
if (getShowCursor() && m_cursor != null)
fieldClicked(m_cursor, modifiedSelect);
return;
}
if ((modifiers & ActionEvent.CTRL_MASK) != 0
|| ! getShowCursor() || m_cursor == null)
return;
boolean shiftModifier = ((modifiers & ActionEvent.SHIFT_MASK) != 0);
GoPoint point = m_cursor;
if (code == KeyEvent.VK_DOWN)
{
point = point.down();
if (shiftModifier)
while (! isHandicapLineOrEdge(point.getY()))
point = point.down();
}
else if (code == KeyEvent.VK_UP)
{
point = point.up(m_size);
if (shiftModifier)
while (! isHandicapLineOrEdge(point.getY()))
point = point.up(m_size);
}
else if (code == KeyEvent.VK_LEFT)
{
point = point.left();
if (shiftModifier)
while (! isHandicapLineOrEdge(point.getX()))
point = point.left();
}
else if (code == KeyEvent.VK_RIGHT)
{
point = point.right(m_size);
if (shiftModifier)
while (! isHandicapLineOrEdge(point.getX()))
point = point.right(m_size);
}
setCursor(point);
}
private void repaint(GoPoint point)
{
m_panel.repaint(point);
}
private void setCursor(GoPoint point, boolean cursor)
{
if (point == null)
return;
Field field = getField(point);
if (field.getCursor() != cursor)
{
field.setCursor(cursor);
repaint(point);
}
}
private void setPreferredFieldSize()
{
int size = (int)((double)GuiUtil.getDefaultMonoFontSize() * 2.5);
if (size % 2 == 0)
++size;
m_preferredFieldSize = new Dimension(size, size);
size = GuiUtil.getDefaultMonoFontSize();
if (size % 2 == 0)
++size;
m_minimumFieldSize = new Dimension(size, size);
}
}