/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk.media.drawing;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.text.StringCharacterIterator;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.wtk.Bounds;
import org.apache.pivot.wtk.HorizontalAlignment;
import org.apache.pivot.wtk.Platform;
import org.apache.pivot.wtk.Theme;
/**
* Shape representing a block of text.
*/
public class Text extends Shape {
private static class TextListenerList extends ListenerList<TextListener>
implements TextListener {
public void textChanged(Text text, String previousText) {
for (TextListener listener : this) {
listener.textChanged(text, previousText);
}
}
public void fontChanged(Text text, Font previousFont) {
for (TextListener listener : this) {
listener.fontChanged(text, previousFont);
}
}
public void widthChanged(Text text, int previousWidth) {
for (TextListener listener : this) {
listener.widthChanged(text, previousWidth);
}
}
public void alignmentChanged(Text text, HorizontalAlignment previousAlignment) {
for (TextListener listener : this) {
listener.alignmentChanged(text, previousAlignment);
}
}
}
private String text = null;
private Font font = DEFAULT_FONT;
private int width = -1;
private HorizontalAlignment alignment = HorizontalAlignment.CENTER;
private ArrayList<GlyphVector> glyphVectors = null;
private TextListenerList textListeners = new TextListenerList();
public static final Font DEFAULT_FONT = new Font("Verdana", Font.PLAIN, 11);
public Text() {
setFill(Color.BLACK);
setStrokeThickness(0);
}
public String getText() {
return text;
}
public void setText(String text) {
String previousText = this.text;
if (previousText != text) {
this.text = text;
invalidate();
textListeners.textChanged(this, previousText);
}
}
public Font getFont() {
return font;
}
public void setFont(Font font) {
if (font == null) {
throw new IllegalArgumentException("font is null.");
}
Font previousFont = this.font;
if (previousFont != font) {
this.font = font;
invalidate();
textListeners.fontChanged(this, previousFont);
}
}
public final void setFont(String font) {
if (font == null) {
throw new IllegalArgumentException("font is null.");
}
if (font.startsWith("{")) {
try {
setFont(Theme.deriveFont(JSONSerializer.parseMap(font)));
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
}
} else {
setFont(Font.decode(font));
}
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
if (width < -1) {
throw new IllegalArgumentException();
}
int previousWidth = this.width;
if (previousWidth != width) {
this.width = width;
invalidate();
textListeners.widthChanged(this, previousWidth);
}
}
public HorizontalAlignment getAlignment() {
return alignment;
}
public void setAlignment(HorizontalAlignment alignment) {
if (alignment == null) {
throw new IllegalArgumentException();
}
HorizontalAlignment previousAlignment = this.alignment;
if (previousAlignment != alignment) {
this.alignment = alignment;
update();
textListeners.alignmentChanged(this, previousAlignment);
}
}
public final void setAlignment(String alignment) {
if (alignment == null) {
throw new IllegalArgumentException();
}
setAlignment(HorizontalAlignment.valueOf(alignment.toUpperCase()));
}
@Override
public boolean contains(int x, int y) {
// TODO Perform hit testing on the glyph vectors themselves
Bounds bounds = getBounds();
return (x >= 0
&& x < bounds.width
&& y >= 0
&& y < bounds.height);
}
@Override
public void draw(Graphics2D graphics) {
int width = getWidth();
// Draw the text
if (glyphVectors.getLength() > 0) {
Paint fill = getFill();
Paint stroke = getStroke();
int strokeThickness = getStrokeThickness();
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
LineMetrics lm = font.getLineMetrics("", fontRenderContext);
float ascent = lm.getAscent();
float y = 0;
for (int i = 0, n = glyphVectors.getLength(); i < n; i++) {
GlyphVector glyphVector = glyphVectors.get(i);
Rectangle2D textBounds = glyphVector.getLogicalBounds();
float lineWidth = (float)textBounds.getWidth();
float x = 0;
switch(alignment) {
case LEFT: {
x = 0;
break;
}
case RIGHT: {
x = width - lineWidth;
break;
}
case CENTER: {
x = (width - lineWidth) / 2;
break;
}
}
if (fill != null) {
graphics.setFont(font);
graphics.setPaint(fill);
graphics.drawGlyphVector(glyphVector, x, y + ascent);
}
// TODO Would caching the outlines help optimize this method, or are they
// already cached by the glyph vector itself?
if (stroke != null
&& strokeThickness > 0) {
java.awt.Shape outline = glyphVector.getOutline();
graphics.setPaint(stroke);
graphics.setStroke(new BasicStroke(strokeThickness));
graphics.translate(x, y + ascent);
graphics.draw(outline);
graphics.translate(-x, -(y + ascent));
}
y += textBounds.getHeight();
}
}
}
@Override
protected void validate() {
if (!isValid()) {
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
glyphVectors = new ArrayList<GlyphVector>();
int width, height;
if (this.width == -1) {
if (text == null
|| text.length() == 0) {
width = 0;
height = 0;
} else {
// Create a single glyph vector representing the entire string
GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, text);
glyphVectors.add(glyphVector);
Rectangle2D textBounds = glyphVector.getLogicalBounds();
width = (int)Math.ceil(textBounds.getWidth());
height = (int)Math.ceil(textBounds.getHeight());
}
} else {
float textWidth = 0;
float textHeight = 0;
int n = text.length();
if (n > 0) {
float lineWidth = 0;
int lastWhitespaceIndex = -1;
int start = 0;
int i = 0;
while (i < n) {
char c = text.charAt(i);
if (Character.isWhitespace(c)) {
lastWhitespaceIndex = i;
}
Rectangle2D characterBounds = font.getStringBounds(text, i, i + 1, fontRenderContext);
lineWidth += characterBounds.getWidth();
if (lineWidth > this.width
&& lastWhitespaceIndex != -1) {
i = lastWhitespaceIndex;
lineWidth = 0;
lastWhitespaceIndex = -1;
// Append the current line
if ((i - 1) - start > 0) {
StringCharacterIterator line = new StringCharacterIterator(text, start, i, start);
GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
glyphVectors.add(glyphVector);
Rectangle2D textBounds = glyphVector.getLogicalBounds();
textWidth = (float)Math.max(textBounds.getWidth(), textWidth);
textHeight += textBounds.getHeight();
}
start = i + 1;
}
i++;
}
// Append the final line
if ((i - 1) - start > 0) {
StringCharacterIterator line = new StringCharacterIterator(text, start, i, start);
GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
glyphVectors.add(glyphVector);
Rectangle2D textBounds = glyphVector.getLogicalBounds();
textWidth = (float)Math.max(textBounds.getWidth(), textWidth);
textHeight += textBounds.getHeight();
}
width = (int)Math.ceil(textWidth);
height = (int)Math.ceil(textHeight);
} else {
width = this.width;
height = 0;
}
}
setBounds(0, 0, width, height);
}
}
public ListenerList<TextListener> getTextListeners() {
return textListeners;
}
}