package bibliothek.chess.view; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; import javax.swing.Icon; import javax.swing.JPanel; import javax.swing.SwingUtilities; import bibliothek.chess.model.Board; import bibliothek.chess.model.ChessListener; import bibliothek.chess.model.Figure; import bibliothek.chess.model.Player; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.DockTheme; import bibliothek.gui.Dockable; import bibliothek.gui.dock.action.DockActionSource; import bibliothek.gui.dock.component.DockComponentConfiguration; import bibliothek.gui.dock.displayer.DisplayerCombinerTarget; import bibliothek.gui.dock.displayer.DisplayerRequest; import bibliothek.gui.dock.dockable.DockableStateListener; import bibliothek.gui.dock.event.DockStationListener; import bibliothek.gui.dock.layout.DockableProperty; import bibliothek.gui.dock.layout.location.AsideRequest; import bibliothek.gui.dock.station.DisplayerCollection; import bibliothek.gui.dock.station.DisplayerFactory; import bibliothek.gui.dock.station.DockableDisplayer; import bibliothek.gui.dock.station.OverpaintablePanel; import bibliothek.gui.dock.station.PlaceholderMapping; import bibliothek.gui.dock.station.StationChildHandle; import bibliothek.gui.dock.station.StationDragOperation; import bibliothek.gui.dock.station.StationDropItem; import bibliothek.gui.dock.station.StationDropOperation; import bibliothek.gui.dock.station.layer.DefaultDropLayer; import bibliothek.gui.dock.station.layer.DockStationDropLayer; import bibliothek.gui.dock.station.support.CombinerTarget; import bibliothek.gui.dock.station.support.PlaceholderMap; import bibliothek.gui.dock.title.ActivityDockTitleEvent; import bibliothek.gui.dock.title.DockTitle; import bibliothek.gui.dock.title.DockTitleRequest; import bibliothek.gui.dock.title.DockTitleVersion; /** * A {@link DockStation} that shows only {@link ChessFigure ChessFigures} as children. * The children are organized in a grid of size 8x8. This grid is colored using * a {@link #setLight(Color) light} and a {@link #setDark(Color) dark} color. * @author Benjamin Sigg */ public class ChessBoard extends OverpaintablePanel implements DockStation, ChessListener { /** Listener which are informed when anything on this station changes */ private List<DockStationListener> listeners = new ArrayList<DockStationListener>(); /** A factory creating instances of {@link DockableDisplayer} used to show instances of {@link ChessFigure}*/ private DisplayerFactory displayerFactory = new ChessDisplayerFactory(); /** The set of currently used {@link DockableDisplayer} */ private DisplayerCollection displayerCollection; /** The controller of the DockingFrames */ private DockController controller; /** The theme used to create graphical elements of this station */ private DockTheme theme; /** The fields of the grid */ private Field[][] fields; /** A list of the fields which are currently occupied by a {@link ChessFigure} */ private List<Field> usedFieldList = new ArrayList<Field>(); /** Information about the next drop-action */ private DropInfo drop; /** * A description of the state of the game. Used to determine which figure * can be moved where, and what information should be painted onto this * {@link ChessBoard}. */ private Board board; /** * A dialog that pops up when a pawn reaches the other side of the board * and has to be replaced by a new figure */ private PawnReplaceDialog pawnReplaceDialog; /** A color used to paint the dark fields of the board */ private Color dark = new Color( 209, 139, 71 ); /** A color used to paint the bright fields of the board */ private Color light = new Color( 255, 206, 158 ); /** * Creates a new board, but does not add any figures to it. * @param pawnReplaceDialog a dialog used to let the user choose a replacement * for a pawn that reaches the last row. */ public ChessBoard( PawnReplaceDialog pawnReplaceDialog ){ this.pawnReplaceDialog = pawnReplaceDialog; setLayout( null ); displayerCollection = new DisplayerCollection( this, displayerFactory, "chess" ); fields = new Field[8][8]; for( int r = 0; r < 8; r++ ) for( int c = 0; c < 8; c++ ) fields[r][c] = new Field( r, c ); setBasePane( new ContentPane() ); setPreferredSize( new Dimension( 8*64, 8*64 ) ); } /** * Sets the model of this ChessBoard. The <code>board</code> contains * information about location and possible moves of every figure. This * ChessBoard will report all actions from the user to <code>board</code>. * @param board the new board, not <code>null</code> */ public void setBoard( Board board ){ if( this.board != null ) this.board.removeListener( this ); this.board = board; for( int r = 0; r < 8; r++ ){ for( int c = 0; c < 8; c++ ){ Figure figure = board.getFigure( r, c ); if( figure != null ) put( r, c, new ChessFigure( figure ) ); else put( r, c, null ); } } board.addListener( this ); revalidate(); repaint(); } /** * Gets the color which is used to fill the dark fields of the grid. * @return the dark color */ public Color getDark(){ return dark; } /** * Sets the color which is used to fill the dark fields of the grid. * @param dark the new color, not <code>null</code> */ public void setDark( Color dark ){ this.dark = dark; repaint(); } /** * Gets the color which is used to fill the bright fields of the grid. * @return the bright color */ public Color getLight(){ return light; } /** * Sets the color which is used to fill the bright fields of the grid * @param light the new color, not <code>null</code> */ public void setLight( Color light ){ this.light = light; repaint(); } public PlaceholderMap getPlaceholders(){ return null; } public void setPlaceholders( PlaceholderMap placeholders ){ // ignore } public PlaceholderMapping getPlaceholderMapping() { throw new UnsupportedOperationException( "not supported" ); } public boolean accept( Dockable child ){ return child instanceof ChessFigure; } public void addDockElementListener( DockableStateListener listener ){ // ignore } public void removeDockElementListener( DockableStateListener listener ){ // ignore } public void addDockStationListener( DockStationListener listener ){ listeners.add( listener ); } public DockComponentConfiguration getComponentConfiguration() { // not required for this example return null; } public void setComponentConfiguration( DockComponentConfiguration configuration ) { // not required for this example } /** * Creates an independent list containing all instances of * {@link DockStationListener} which are currently registered. * @return the new list */ protected DockStationListener[] listListeners(){ return listeners.toArray( new DockStationListener[ listeners.size() ] ); } public boolean canDrag( Dockable dockable ){ ChessFigure figure = (ChessFigure)dockable; return figure.getFigure().getPlayer() == board.getPlayer(); } public boolean canReplace( Dockable old, Dockable next ){ return false; } public void changed( Dockable dockable, DockTitle title, boolean active ){ title.changed( new ActivityDockTitleEvent( dockable, active ) ); } public void requestChildDockTitle( DockTitleRequest request ){ // ignore } public void requestChildDisplayer( DisplayerRequest request ){ // ignore } public void drag( Dockable dockable ){ Field field = getFieldOf( dockable ); if( field != null ) put( field.getRow(), field.getColumn(), null ); } public void drop( Dockable dockable ){ throw new IllegalStateException( "Can't just drop a figure on a chess board" ); } public boolean drop( Dockable dockable, DockableProperty property ){ if( property instanceof ChessBoardProperty ){ ChessBoardProperty location = (ChessBoardProperty)property; put( location.getRow(), location.getColumn(), (ChessFigure)dockable ); return true; } return false; } /** * Ensures that the field <code>row/column</code> shows <code>figure</code>. * @param row the row of the field * @param column the column of the field * @param figure the figure to show, can be <code>null</code> if the field * should be cleared. */ public void put( int row, int column, ChessFigure figure ){ Field field = fields[row][column]; ChessFigure old = field.getFigure(); if( old != null ){ for( DockStationListener listener : listListeners() ) listener.dockableRemoving( this, old ); field.set( null ); old.setDockParent( null ); for( DockStationListener listener : listListeners() ) listener.dockableRemoved( this, old ); } if( figure != null ){ Field oldField = getFieldOf( figure ); if( oldField == null ){ for( DockStationListener listener : listListeners() ) listener.dockableAdding( this, figure ); field.set( figure ); figure.setDockParent( this ); for( DockStationListener listener : listListeners() ) listener.dockableAdded( this, figure ); } else{ field.transfer( oldField ); if( !board.isEmpty( oldField.getRow(), oldField.getColumn() )){ board.move( oldField.getRow(), oldField.getColumn(), row, column ); Figure pawn = board.pawnReplacement(); if( pawn != null ){ pawn = pawnReplaceDialog.replace( pawn ); board.put( pawn ); figure.setFigure( pawn ); } board.switchPlayer(); } } } } /** * Gets the field which shows <code>figure</code>. * @param figure the figure whose field is searched * @return the field or <code>null</code> if nothing was found */ private Field getFieldOf( Dockable figure ){ for( Field field : usedFieldList ) if( field.getFigure() == figure ) return field; return null; } public DockController getController(){ return controller; } public DockActionSource getDirectActionOffers( Dockable dockable ){ // no actions return null; } public Dockable getDockable( int index ){ return usedFieldList.get( index ).getFigure(); } public int getDockableCount(){ return usedFieldList.size(); } public DockableProperty getDockableProperty( Dockable dockable, Dockable ignored ){ Field field = getFieldOf( dockable ); return new ChessBoardProperty( field.getRow(), field.getColumn() ); } public void aside( AsideRequest request ){ // no support for this feature required } public Dockable getFrontDockable(){ return null; } public DockActionSource getIndirectActionOffers( Dockable dockable ){ // no offers from this station return null; } public Rectangle getStationBounds(){ Point location = new Point( 0, 0 ); SwingUtilities.convertPointToScreen( location, this ); return new Rectangle( location.x, location.y, getWidth(), getHeight() ); } public DockTheme getTheme(){ return theme; } public boolean isStationShowing(){ return isStationVisible(); } @Deprecated public boolean isStationVisible(){ return isShowing(); } public boolean isChildShowing( Dockable dockable ){ return isVisible( dockable ); } @Deprecated public boolean isVisible( Dockable dockable ){ return isStationVisible(); } public DockStationDropLayer[] getLayers(){ return new DockStationDropLayer[]{ new DefaultDropLayer( this ) }; } public void move( Dockable dockable, DockableProperty property ) { // do nothing } public StationDragOperation prepareDrag( Dockable dockable ){ // do nothing return null; } public StationDropOperation prepareDrop( StationDropItem item ){ return prepare( item.getMouseX(), item.getMouseY(), (ChessFigure)item.getDockable() ); } /** * Prepares to move or drop a figure on this board. * @param x the x-coordinate of the mouse * @param y the y-coordinate of the mouse * @param figure the figure which is grabbed * @return <code>true</code> if a valid location for <code>figure</code> * was found, <code>false</code> otherwise */ private StationDropOperation prepare( int x, int y, ChessFigure figure ){ Point location = new Point( x, y ); SwingUtilities.convertPointFromScreen( location, this ); int w = getWidth(); int h = getHeight(); if( location.x < 0 || location.y < 0 || location.x > w || location.y > h ){ return null; } int r = -1; int c = 7; while( c*w/8 > location.x ) c--; while( (7-r)*h/8 > location.y ) r++; final DropInfo drop = new DropInfo(); figure.getFigure().reachable( new Board.CellVisitor(){ public boolean visit( int r, int c, Figure figure ) { drop.targets[r][c] = true; return true; } }); drop.figure = figure; drop.row = r; drop.column = c; drop.valid = drop.targets[drop.row][drop.column]; return drop; } public void removeDockStationListener( DockStationListener listener ){ listeners.remove( listener ); } public void replace( DockStation old, Dockable next ){ replace( old.asDockable(), next ); } public void replace( Dockable old, Dockable next ){ Field field = getFieldOf( old ); if( field == null ) throw new IllegalArgumentException( "Unknown dockable" ); put( field.getRow(), field.getColumn(), (ChessFigure)next ); } public void setController( DockController controller ){ this.controller = controller; controller.getDockTitleManager().registerDefault( "chess-board", ChessDockTitle.FACTORY ); displayerCollection.setController( controller ); for( Field field : usedFieldList ) field.updateTitle(); } public void setFrontDockable( Dockable dockable ){ // ignore } public void updateTheme(){ if( controller != null ){ this.theme = controller.getTheme(); for( Field field : usedFieldList ) field.updateTitle(); revalidate(); } } public DockStation asDockStation(){ return this; } public Dockable asDockable(){ return null; } public String getFactoryID(){ return "chess-board"; } public void killed( int r, int c, Figure figure ) { // ensure removed ChessFigure view = fields[r][c].getFigure(); if( view != null && view.getFigure() == figure ) put( r, c, null ); } public void moved( int sr, int sc, int dr, int dc, Figure figure ) { ChessFigure view = fields[sr][sc].getFigure(); if( view != null && view.getFigure() == figure ) put( dr, dc, view ); } public void playerSwitched( Player player ) { // ignore } /** * Calculates the smallest x-coordinate which is still part of the column <code>c</code>. * @param c a column * @return the smallest x-coordinate in c */ private int x( int c ){ return c*getWidth()/8; } /** * Calculates the smallest y-coordinate which is still part of the row <code>r</code>. * @param r a row * @return the smallest y-coordinate in r */ private int y( int r ){ return (7-r)*getHeight()/8; } /** * Calculates the width of column <code>c</code>. * @param c a column * @return the width */ private int w( int c ){ return x( c+1 ) - x( c ); } /** * Calculates the height of row <code>r</code>. * @param r a row * @return the height */ private int h( int r ){ return y( r-1 ) - y( r ); } @Override protected void paintOverlay( Graphics g ){ if( drop != null ){ final Graphics2D g2 = (Graphics2D)g.create(); g2.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.5f ) ); g2.setColor( Color.GREEN ); for( int r = 0; r < 8; r++ ){ for( int c = 0; c < 8; c++ ){ if( drop.targets[r][c] ){ g2.fillRect( x( c ), y( r ), w( c ), h( r ) ); } } } int r = drop.row; int c = drop.column; if( !drop.valid ){ g2.setColor( Color.RED ); g2.fillRect( x( c ), y( r ), w( c ), h( r ) ); } Icon icon = drop.figure.getFigure().getBigIcon(); icon.paintIcon( this, g2, x(c)+(w(c)-icon.getIconWidth())/2, y(r) + (h(r)-icon.getIconHeight())/2 ); g2.dispose(); } } public boolean isAutoRemoveable(){ return false; } /** * An instance of DropInfo contains all information needed to execute * a drag and drop-action on a {@link ChessBoard}. * @author Benjamin Sigg */ private class DropInfo implements StationDropOperation{ /** the figure which is grabbed */ public ChessFigure figure = null; /** the target row */ public int row; /** the target column */ public int column; /** whether the target is a valid destination or not */ public boolean valid; /** which fields are valid targets */ public boolean[][] targets = new boolean[8][8]; public boolean isMove(){ return true; } public void draw(){ drop = this; repaint(); } public void destroy( StationDropOperation next ){ if( drop == this ){ drop = null; repaint(); } } public void execute(){ if( valid ){ put( row, column, figure ); } } public CombinerTarget getCombination(){ return null; } public DisplayerCombinerTarget getDisplayerCombination(){ return null; } public DockStation getTarget(){ return ChessBoard.this; } public Dockable getItem(){ return figure; } } /** * A panel that paints a 8x8 grid and shows the children of a {@link ChessBoard}. * @author Benjamin Sigg */ private class ContentPane extends JPanel{ @Override protected void paintComponent( Graphics g ){ for( int r = 0; r < 8; r++ ){ for( int c = 0; c < 8; c++ ){ if( (r+c) % 2 == 0 ) g.setColor( dark ); else g.setColor( light ); g.fillRect( x( c ), y( r ), w( c ), h( r ) ); } } } @Override public void doLayout(){ for( Field field : usedFieldList ){ Component component = field.getHandle().getDisplayer().getComponent(); int r = field.getRow(); int c = field.getColumn(); component.setBounds( x(c), y(r), w(c), h(r) ); } } } /** * A field of the grid of a {@link ChessBoard}. A field contains all * information needed to paint it. * @author Benjamin Sigg */ private class Field{ /** the displayer is used to show <code>figure</code> */ private StationChildHandle handle; /** the figure on this field, might be <code>null</code> */ private ChessFigure figure; /** the row in which this field lies */ private int row; /** the column in which this field lies */ private int column; /** * Creates a new field. * @param row the row in which this field lies * @param column the column in which this field lies */ public Field( int row, int column ){ this.row = row; this.column = column; } /** * Gets the row in which this field lies. * @return the row */ public int getRow(){ return row; } /** * Gets the column in which this field lies. * @return the column */ public int getColumn(){ return column; } /** * Gets the element and its visualization. * @return the figure or <code>null</code> */ public StationChildHandle getHandle(){ return handle; } /** * Gets the figure which is shown on this field. * @return the figure, may be <code>null</code> */ public ChessFigure getFigure(){ return figure; } /** * Moves the figure of this field to the empty field <code>other</code>. * @param other the destination */ public void transfer( Field other ){ set( null ); if( other.figure != null ){ this.handle = other.handle; this.figure = other.figure; other.handle = null; other.figure = null; usedFieldList.remove( other ); usedFieldList.add( this ); revalidate(); } } /** * Sets the {@link Dockable} which is shown on this field. * @param figure the new dockable, might be <code>null</code> */ public void set( ChessFigure figure ){ ChessFigure old = this.figure; if( this.figure != null ){ getContentPane().remove( handle.getDisplayer().getComponent() ); handle.destroy(); this.figure = null; this.handle = null; } this.figure = figure; if( this.figure != null ){ DockTitleVersion version = null; if( controller != null ) version = controller.getDockTitleManager().getVersion( "chess-board" ); handle = new StationChildHandle( ChessBoard.this, displayerCollection, figure, version ); handle.updateDisplayer(); getContentPane().add( handle.getDisplayer().getComponent() ); } if( old == null && figure != null ) usedFieldList.add( this ); else if( old != null && figure == null ) usedFieldList.remove( this ); getContentPane().revalidate(); } /** * Ensures that the correct {@link DockTitle} for the current * {@link #set(ChessFigure) figure} is shown. */ public void updateTitle(){ if( handle != null ){ if( controller == null ){ handle.setTitleRequest( null ); } else{ DockTitleVersion version = controller.getDockTitleManager().getVersion( "chess-board" ); handle.setTitleRequest( version ); } } } } }