/*
* Copyright (C) 2011 Alasdair C. Hamilton
*
* 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 3 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package ket.display.box;
import geom.Offset;
import geom.Position;
import java.awt.*;
import java.util.*;
import ket.math.*;
import ket.display.ColourScheme;
/*
* handle display of a letter, symbol or word
*/
public class BoxText extends Box {
/**
* The font used to draw the text.
*/
Font font;
/**
* The entire text before it is split into multiple lines.
*/
final String text;
/**
* The text after it has been split into multiple lines.
*/
Vector<String> lines;
/**
* The height above the text's coordinate at which the top of the text
* is drawn.
*/
double ascent;
/**
* The distance downwards from the text's coordinate to the bottom of the text.
*/
double decent;
/**
* For each line of text, record a series of line lengths (in pixels)
* and their corresponding index in the 'text' string.
*/
Vector<TreeMap<Integer, Integer>> wordWidthsToIndexList = null; // index against width.
//- Position topLeft = null;
Offset actualSize = null;
// TODO: When is box.clone used()? Does it require that lines also be
// passed in as an argument?
@Override
public Box cloneBox() {
return new BoxText(getArgument(), text, getSettings());
}
@Override
protected void initDefaultSettings(long settings) {
horizontalAlignment = X_CENTRE_ALIGN;
verticalAlignment = Y_CENTRE_ALIGN;
preserveAspectRatio = false;
relativeFontSize = NORMAL_FONT;
// Most box descendents default to italics, but text should be plain.
style = PLAIN_FONT;
}
public BoxText(Argument argument, String text, long settings) {
super(argument, settings);
assert text!=null : "The text string displayed in BoxText can't be null";
this.text = text;
this.lines = new Vector<String>();
this.font = null;
this.ascent = 0.0;
}
/*
* Change the font to a given size and style.
*/
@Override
protected void fontSetup(int parentFontSize) {
super.fontSetup(parentFontSize); // Note: this isn't used.
int styleFontFlag = 0;
// Text defaults to plain while word defaults to italics.
if (hasProperty(BOLD_FONT)) {
styleFontFlag |= Font.BOLD;
} else if (hasProperty(ITALIC_FONT)) {
styleFontFlag |= Font.ITALIC;
} else {
styleFontFlag |= Font.PLAIN;
}
font = new Font( //! Font.DIALOG,
"Times New Roman",
styleFontFlag,
(int) (TEXT_SCALE_FACTOR*fontSize));
boolean invalid = false;
for (int i=0; i<text.length(); i++) {
int codePoint = text.codePointAt(i);
if ( ! font.canDisplay(codePoint) ) {
invalid = true;
}
}
if (invalid) {
font = new Font(
Font.DIALOG,
styleFontFlag,
(int) (TEXT_SCALE_FACTOR*fontSize));
}
}
/*
* Use the font to determine the size of the text.
*/
@Override
protected void calcMinimumSize() {
FontMetrics fontMetrics = getFontMetrics(font);
int width = fontMetrics.stringWidth(text);
ascent = fontMetrics.getAscent();
decent = fontMetrics.getDescent();
innerRectangle = new Offset(width, decent+ascent);
}
public boolean isWhitespace(String currentLineSoFar) {
return currentLineSoFar.trim().isEmpty();
}
@Override
public void setupOuterRectangle(Offset actualSize) {
FontMetrics fontMetrics = getFontMetrics(font);
boolean hasMultipleLines = innerRectangle.width>=actualSize.width;
this.actualSize = actualSize;
wordWidthsToIndexList = new Vector<TreeMap<Integer, Integer>>();
lines = new Vector<String>();
String currentLineSoFar = "";
int lengthOfPreviousLines = 0;
for (int i=0; i<text.length(); i++) {
String suffix = getNextWord(i, text);
i += (i==text.length()?0:-1) + suffix.length();
int width = fontMetrics.stringWidth(currentLineSoFar + suffix);
if (width > actualSize.width) { // wrap line.
updateMap(fontMetrics, currentLineSoFar, lengthOfPreviousLines);
lines.add(currentLineSoFar);
lengthOfPreviousLines += currentLineSoFar.length();
currentLineSoFar = suffix;
} else {
currentLineSoFar += suffix;
}
if (isWhitespace(currentLineSoFar)) { // Avoid indentation from whitespace.
currentLineSoFar = "";
}
}
updateMap(fontMetrics, currentLineSoFar, lengthOfPreviousLines);
lines.add(currentLineSoFar);
int decent = fontMetrics.getDescent();
double textHeight = lines.size() * (ascent + decent);
if (hasMultipleLines) {
innerRectangle = new Offset(actualSize.width, textHeight);
}
double largestHeight = Math.max(actualSize.height, textHeight);
Offset largestSize = new Offset(actualSize.width, largestHeight);
super.setupOuterRectangle(largestSize);
}
private void updateMap(FontMetrics fontMetrics, String currentLineSoFar, int lengthOfPreviousLines) {
TreeMap<Integer, Integer> wordWidthsToIndex = new TreeMap<Integer, Integer>();
for (int q=1; q<currentLineSoFar.length(); q++) {
int fullWidth = fontMetrics.stringWidth(currentLineSoFar.substring(0, q));
wordWidthsToIndex.put(fullWidth, lengthOfPreviousLines+q);
}
wordWidthsToIndexList.add(wordWidthsToIndex);
}
/**
* If the i'th index of text is the start of a word, return the word,
* and otherwise return just the character.
*/
private String getNextWord(int i, String text) {
String word = "";
for ( ; i<text.length(); i++) {
char c = text.charAt(i);
word += c;
//-? if (!Character.isLetter(c) && c!='|') { // cursor is '|' so don't wrap mid-way through the current word.
if (c==' ') {
return word;
}
}
return word;
}
private int getFontProperty() {
if (hasProperty(BOLD_FONT)) {
return Font.BOLD;
} else if (hasProperty(PLAIN_FONT)) {
return Font.PLAIN;
} else {
return Font.ITALIC;
}
}
/**
* If specified, return the index corresponding to the given location.
*/
public int getIndex(Position p) {
if (topLeft==null || actualSize==null || wordWidthsToIndexList==null) {
return -1;
}
int row = (int) (p.y - getYPosition(topLeft)) / (int) actualSize.height;
if (row<0 || row >= wordWidthsToIndexList.size()) {
return -1;
}
TreeMap<Integer, Integer> wordWidthsToIndex = wordWidthsToIndexList.get(row);
int oldWidth = 0;
int boxIndent = (int) getXPosition(topLeft);
for (int width : wordWidthsToIndex.navigableKeySet()) {
boolean bounded = 0<=p.x && p.x<boxIndent+width;
if (bounded) {
Integer index = wordWidthsToIndex.get(width); // Don't always 'round' the mouse click location 'up' (i.e. right).
if (index==null) { // just in case.
return -1;
}
//? return index;
double oldSeparation = p.x - boxIndent - width;
double separation = boxIndent + oldWidth - p.x;
return oldSeparation<separation ? index-1 : index;
}
oldWidth = width;
}
return -1;
}
@Override
public void draw(Graphics2D g2D, Position topLeft, ColourScheme colourScheme) {
this.topLeft = topLeft; //?
double x = getXPosition(topLeft);
double y = getYPosition(topLeft);
g2D.setFont(font);
for (int i=0; i<lines.size(); i++) {
float yShift = (float) (y + i*(ascent+decent));
float yPos = (float) (yShift + ascent);
g2D.drawString(lines.get(i), (float) x, yPos);
}
}
public String getText() {
return text;
}
public String toString() {
return "BoxText("+lines+" | "+super.toString();
}
}