/*
* File : TextGrid.java
* Created : 10-sep-2001 13:08
* By : fbusquets
*
* JClic - Authoring and playing system for educational activities
*
* Copyright (C) 2000 - 2005 Francesc Busquets & Departament
* d'Educacio de la Generalitat de Catalunya
*
* This program 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; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 (see the LICENSE file).
*/
package edu.xtec.jclic.boxes;
import edu.xtec.util.StrUtils;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Rectangle2D.Double;
import java.awt.image.ImageObserver;
import javax.swing.JComponent;
import javax.swing.RepaintManager;
import javax.swing.Timer;
/**
* This class is a special {@link edu.xtec.jclic.boxes.AbstractBox} that displays a grid
* of single characters. It is used in activities like crosswords and scrambled letters.
* @author Francesc Busquets (fbusquets@xtec.cat)
* @version 13.08.08
*/
public class TextGrid extends AbstractBox implements Cloneable, Resizable, ActionListener{
int nRows, nCols;
char [][] chars;
char [][] answers;
int [][] attributes;
double cellWidth;
double cellHeight;
Rectangle2D preferredBounds=new Double();
public char wild=TextGridContent.DEFAULT_WILD;
String randomChars=TextGridContent.DEFAULT_RANDOM_CHARS;
boolean cursorEnabled;
boolean useCursor;
Point cursor=new Point();
boolean cursorBlink;
Timer cursorTimer;
boolean wildTransparent;
public static final int MIN_CELL_SIZE=12, DEFAULT_CELL_SIZE=20, MIN_INTERNAL_MARGIN=2;
public static final int NORMAL=0, INVERTED=1, HIDDEN=2, LOCKED=4, MARKED=8, TRANSPARENT=16;
/** Creates new TextGridBox */
public TextGrid(AbstractBox parent, JComponent container, double setX, double setY,
int setNcw, int setNch, double setCellW, double setCellH,
BoxBase boxBase, boolean setBorder){
super(parent, container, boxBase);
x=setX;
y=setY;
nCols=Math.max(1, setNcw);
nRows=Math.max(1, setNch);
cellWidth=Math.max(setCellW, MIN_CELL_SIZE);
cellHeight=Math.max(setCellH, MIN_CELL_SIZE);
width=cellWidth*nCols;
height=cellHeight*nRows;
chars=new char[nRows][nCols];
attributes=new int[nRows][nCols];
preferredBounds.setRect(getBounds());
setBorder(setBorder);
cursorTimer=new Timer(500, this);
cursorTimer.setRepeats(true);
cursorEnabled=false;
useCursor=false;
wildTransparent=false;
answers=null;
}
public static TextGrid createEmptyGrid(AbstractBox parent, JComponent container,
double setX, double setY, TextGridContent tgc,
boolean wildTransparent){
TextGrid result=new TextGrid(parent, container, setX, setY,
tgc.ncw, tgc.nch, tgc.w, tgc.h, tgc.bb, tgc.border);
result.wild=tgc.wild;
result.randomChars=tgc.randomChars;
result.wildTransparent=wildTransparent;
return result;
}
public void setChars(String[] text){
answers=new char[nRows][nCols];
for(int py=0; py<nRows; py++){
String line = py<text.length ? text[py] : null;
for(int px=0; px<nCols; px++){
chars[py][px]=(line==null || px>=line.length()) ? ' ' : line.charAt(px);
answers[py][px]=chars[py][px];
}
}
repaint();
}
public void randomize(){
for(int py=0; py<nRows; py++)
for(int px=0; px<nCols; px++)
if(chars[py][px]==wild)
chars[py][px]=randomChars.charAt((int)(Math.random()*randomChars.length()));
repaint();
}
public void setCellAttributes(boolean lockWild, boolean clearChars){
int atr=LOCKED;
if(wildTransparent)
atr|=TRANSPARENT;
else
atr|=INVERTED|HIDDEN;
for(int py=0; py<nRows; py++)
for(int px=0; px<nCols; px++)
if(lockWild && chars[py][px]==wild)
attributes[py][px]=atr;
else{
attributes[py][px]=NORMAL;
if(clearChars)
chars[py][px]=' ';
}
repaint();
}
public void setCellLocked(int px, int py, boolean locked){
if(px>=0 && px<nCols && py>=0 && py<nRows){
attributes[py][px] = locked
? LOCKED | (wildTransparent ? TRANSPARENT : INVERTED|HIDDEN)
: NORMAL;
}
}
public Point getItemFor(int rx, int ry){
if(!isValidCell(rx, ry))
return null;
Point point=new Point();
boolean inBlack=false, startCount=false;
for(int px=0; px<rx; px++){
if((attributes[ry][px]&LOCKED)!=0){
if(!inBlack){
if(startCount)
point.x++;
inBlack=true;
}
} else{
startCount=true;
inBlack=false;
}
}
inBlack=false;
startCount=false;
for(int py=0; py<ry; py++){
if((attributes[py][rx]&LOCKED)!=0){
if(!inBlack){
if(startCount)
point.y++;
inBlack=true;
}
} else{
startCount=true;
inBlack=false;
}
}
return point;
}
public void setCursorEnabled(boolean status){
cursorEnabled=status;
if(status==true)
startCursorBlink();
else
stopCursorBlink();
}
public void startCursorBlink(){
if(useCursor && cursorEnabled && cursorTimer!=null && !cursorTimer.isRunning()){
blink(1);
cursorTimer.start();
}
}
public void stopCursorBlink(){
if(cursorTimer!=null && cursorTimer.isRunning()){
cursorTimer.stop();
blink(-1);
}
}
public void moveCursor(int dx, int dy, boolean skipLocked){
if(useCursor){
Point point=findNextCellWithAttr(cursor.x, cursor.y,
skipLocked ? LOCKED : NORMAL,
dx, dy, false);
if(!cursor.equals(point))
setCursorAt(point.x, point.y, skipLocked);
}
}
public Point findFreeCell(Point from, int dx, int dy){
Point result=null;
if(from!=null && (dx!=0 || dy!=0)){
Point scan=new Point(from);
while(result==null){
scan.x+=dx;
scan.y+=dy;
if(scan.x<0 || scan.x>=nCols || scan.y<0 || scan.y>=nRows)
break;
if(!getCellAttribute(scan.x, scan.y, LOCKED))
result=scan;
}
}
return result;
}
public boolean isIntoBlacks(Point pt, boolean checkHorizontal){
boolean result;
if(checkHorizontal){
result=(pt.x<=0 || getCellAttribute(pt.x-1, pt.y, LOCKED))
&& (pt.x>=nCols-1 || getCellAttribute(pt.x+1, pt.y, LOCKED));
}
else{
result=(pt.y<=0 || getCellAttribute(pt.x, pt.y-1, LOCKED))
&& (pt.y>=nRows-1 || getCellAttribute(pt.x, pt.y+1, LOCKED));
}
return result;
}
public boolean isIntoWhites(Point pt, boolean checkHorizontal){
boolean result;
if(checkHorizontal){
result=(pt.x>0 && !getCellAttribute(pt.x-1, pt.y, LOCKED))
&& (pt.x<nCols-1 && !getCellAttribute(pt.x+1, pt.y, LOCKED));
}
else{
result=(pt.y>0 && !getCellAttribute(pt.x, pt.y-1, LOCKED))
&& (pt.y<nRows-1 && !getCellAttribute(pt.x, pt.y+1, LOCKED));
}
return result;
}
public Point findNextCellWithAttr(int startX, int startY, int attr,
int dx, int dy, boolean attrState){
Point point=new Point(startX+dx, startY+dy);
while(true){
if(point.x<0){
point.x=nCols-1;
if(point.y>0)
point.y--;
else
point.y=nRows-1;
}
else if(point.x>=nCols){
point.x=0;
if(point.y<nRows-1)
point.y++;
else
point.y=0;
}
if(point.y<0){
point.y=nRows-1;
if(point.x>0)
point.x--;
else
point.x=nCols-1;
}
else if(point.y>=nRows){
point.y=0;
if(point.x<nCols-1)
point.x++;
else
point.x=0;
}
if((point.x==startX && point.y==startY) ||
getCellAttribute(point.x, point.y, attr)==attrState)
break;
point.x+=dx;
point.y+=dy;
}
return point;
}
public void setCursorAt(int px, int py, boolean skipLocked){
stopCursorBlink();
if(isValidCell(px, py)){
cursor.x=px; cursor.y=py;
useCursor=true;
if(skipLocked && getCellAttribute(px, py, LOCKED)){
moveCursor(1, 0, skipLocked);
}
else{
if(cursorEnabled)
startCursorBlink();
}
}
}
public void setUseCursor(boolean value){
useCursor=value;
}
public Point getCursor(){
return cursor;
}
public int countCharsLike(char ch){
int result=0;
for(int py=0; py<nRows; py++)
for(int px=0; px<nCols; px++)
if(chars[py][px]==ch)
result++;
return result;
}
public int getNumCells(){
return nRows*nCols;
}
public int countCoincidences(boolean checkCase){
int result=0;
if(answers!=null)
for(int py=0; py<nRows; py++)
for(int px=0; px<nCols; px++)
if(isCellOk(px, py, checkCase))
result++;
return result;
}
public boolean isCellOk(int px, int py, boolean checkCase){
boolean result=false;
if(isValidCell(px, py)){
char ch=chars[py][px];
if(ch!=wild){
char ch2=answers[py][px];
if(ch==ch2 ||
(!checkCase && Character.toUpperCase(ch)==Character.toUpperCase(ch2)))
result=true;
}
}
return result;
}
public Point getLogicalCoords(Point2D devicePoint){
if(!contains(devicePoint))
return null;
int px=(int)((devicePoint.getX()-getX())/cellWidth);
int py=(int)((devicePoint.getY()-getY())/cellHeight);
if(isValidCell(px, py)){
return new Point(px, py);
}
else
return null;
}
public boolean isValidCell(int px, int py){
return px<nCols && py<nRows && px>=0 && py>=0;
}
public void setCharAt(int px, int py, char ch){
if(isValidCell(px, py)){
chars[py][px]=ch;
repaintCell(px, py);
}
}
public char getCharAt(int px, int py){
if(isValidCell(px, py))
return chars[py][px];
else
return ' ';
}
public String getStringBetween(int x0, int y0, int x1, int y1){
StringBuilder sb=new StringBuilder();
if(isValidCell(x0, y0) && isValidCell(x1, y1)){
int dx=x1-x0;
int dy=y1-y0;
if(dx==0 || dy==0 || Math.abs(dx)==Math.abs(dy)){
int steps=Math.max(Math.abs(dx), Math.abs(dy));
if(steps>0){
dx/=steps;
dy/=steps;
}
for(int i=0; i<=steps; i++)
sb.append(getCharAt(x0+dx*i, y0+dy*i));
}
}
return sb.substring(0);
}
public void setAttributeBetween(int x0, int y0, int x1, int y1, int attribute, boolean value){
if(isValidCell(x0, y0) && isValidCell(x1, y1)){
int dx=x1-x0;
int dy=y1-y0;
if(dx==0 || dy==0 || Math.abs(dx)==Math.abs(dy)){
int steps=Math.max(Math.abs(dx), Math.abs(dy));
if(steps>0){
dx/=steps;
dy/=steps;
}
for(int i=0; i<=steps; i++)
setAttribute(x0+dx*i, y0+dy*i, attribute, value);
}
}
}
public void setAttribute(int px, int py, int attribute, boolean state){
if(isValidCell(px, py)){
if(attribute==MARKED && !state)
repaintCell(px, py);
attributes[py][px]&=~attribute;
attributes[py][px]|=(state ? attribute : 0);
if(attribute!=MARKED || state)
repaintCell(px, py);
}
}
public void setAllCellsAttribute(int attribute, boolean state){
for(int py=0; py<nRows; py++)
for(int px=0; px<nCols; px++)
setAttribute(px, py, attribute, state);
}
public boolean getCellAttribute(int px, int py, int attribute){
if(isValidCell(px, py))
return (attributes[py][px]&attribute)!=0;
else
return false;
}
public Rectangle2D getCellRect(int px, int py){
return new Double(getX()+px*cellWidth, getY()+py*cellHeight, cellWidth, cellHeight);
}
public Rectangle getCellBorderBounds(int px, int py){
boolean isMarked=getCellAttribute(px, py, MARKED);
if(!border && !isMarked)
return getCellRect(px, py).getBounds();
BoxBase bb=getBoxBaseResolve();
Stroke strk=isMarked ? bb.getMarker() : bb.getBorder();
return strk.createStrokedShape(getCellRect(px, py)).getBounds();
}
public void repaintCell(int px, int py){
JComponent jc=getContainerResolve();
if(jc!=null)
jc.repaint(getCellBorderBounds(px, py));
}
@Override
public Object clone(){
TextGrid tgb=(TextGrid)super.clone();
tgb.nRows=nRows;
tgb.nCols=nCols;
tgb.chars=new char[nRows][nCols];
tgb.attributes=new int[nRows][nCols];
for(int i=0; i<nRows; i++){
System.arraycopy(chars[i], 0, tgb.chars[i], 0, nCols);
System.arraycopy(attributes[i], 0, tgb.attributes[i], 0, nCols);
}
tgb.cellWidth=cellWidth;
tgb.cellHeight=cellHeight;
tgb.preferredBounds=(Rectangle2D)preferredBounds.clone();
return tgb;
}
public Dimension getPreferredSize(){
return preferredBounds.getBounds().getSize();
}
public Dimension getMinimumSize(){
return new Dimension(MIN_CELL_SIZE * nCols, MIN_CELL_SIZE * nRows);
}
public Dimension getScaledSize(double scale){
return new Dimension(StrUtils.roundTo(scale*preferredBounds.getWidth(), nCols),
StrUtils.roundTo(scale*preferredBounds.getHeight(), nRows));
}
@Override
public void setBounds(Rectangle2D r){
super.setBounds(r);
cellWidth=width/nCols;
cellHeight=height/nRows;
}
@Override
public boolean update(Graphics2D g2, Rectangle dirtyRegion, ImageObserver io){
if(isEmpty() || !isVisible() || isTemporaryHidden())
return false;
if(dirtyRegion!=null && !shape.intersects(dirtyRegion))
return false;
updateContent(g2, dirtyRegion, io);
return true;
}
public boolean updateContent(Graphics2D g2, Rectangle dirtyRegion, ImageObserver io){
FontRenderContext frc=g2.getFontRenderContext();
BoxBase bb=getBoxBaseResolve();
// test font size
FontMetrics fm=g2.getFontMetrics(bb.getFont());
boolean resize=false;
while(true){
if(fm.charWidth('W')<=cellWidth-2*MIN_INTERNAL_MARGIN &&
fm.getAscent()+fm.getDescent()<=cellHeight-2*MIN_INTERNAL_MARGIN)
break;
if(bb.reduceFont()==false)
break;
resize=true;
fm=g2.getFontMetrics(bb.getFont());
}
if(resize){
JComponent jc=getContainerResolve();
if(jc!=null)
RepaintManager.currentManager(jc).markCompletelyDirty(jc);
return true;
}
char[] ch=new char[1];
int attr;
boolean isMarked, isInverted, isCursor;
Rectangle2D boxBounds;
double dx, dy;
double ry=(cellHeight-fm.getDescent()+fm.getAscent())/2;
for(int py=0; py<nRows; py++){
for(int px=0; px<nCols; px++){
Rectangle bxr=getCellBorderBounds(px, py);
if(bxr.intersects(dirtyRegion)){
attr=attributes[py][px];
if((attr&TRANSPARENT)==0){
isInverted=(attr&INVERTED)!=0;
isMarked=(attr&MARKED)!=0;
isCursor=(useCursor && cursor.x==px && cursor.y==py);
boxBounds=getCellRect(px, py);
g2.setColor((isCursor && cursorBlink) ?
bb.inactiveColor :
isInverted ? bb.textColor : bb.backColor);
g2.fill(boxBounds);
g2.setColor(Color.black);
if((attr&HIDDEN)==0){
ch[0]=chars[py][px];
if(ch[0]!=0){
dx=boxBounds.getX()+(cellWidth-fm.charWidth(ch[0]))/2;
dy=boxBounds.getY()+ry;
GlyphVector gv=bb.getFont().createGlyphVector(frc, ch);
if(bb.shadow){
g2.setColor(bb.shadowColor);
g2.drawGlyphVector(gv, (float)(dx+bb.getDynFontSize()/10),
(float)(dy+bb.getDynFontSize()/10));
}
g2.setColor(isInverted ?
bb.backColor :
isAlternative() ? bb.alternativeColor : bb.textColor);
g2.drawGlyphVector(gv, (float)dx, (float)dy);
}
}
if(border || isMarked){
g2.setColor(bb.borderColor);
g2.setStroke(isMarked ? bb.getMarker() : bb.getBorder());
if(isMarked)
g2.setXORMode(Color.white);
g2.draw(boxBounds);
if(isMarked)
g2.setPaintMode();
g2.setStroke(BoxBase.DEFAULT_STROKE);
}
g2.setColor(Color.black);
}
}
}
}
return true;
}
public void actionPerformed(ActionEvent ev){
blink(0);
}
protected synchronized void blink(int status){
if(useCursor){
cursorBlink = status==1 ? true : status==-1 ? false : !cursorBlink;
repaintCell(cursor.x, cursor.y);
}
}
@Override
public void end(){
if(cursorTimer!=null){
cursorTimer.stop();
cursorTimer=null;
}
}
public void finalize() throws Throwable{
try {
end();
} finally {
super.finalize();
}
}
}