/*
* Copyright (c) 2005 Matthew Hall and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthew Hall - initial API and implementation
*/
package org.eclipse.nebula.paperclips.core.text;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.nebula.paperclips.core.PaperClips;
import org.eclipse.nebula.paperclips.core.Print;
import org.eclipse.nebula.paperclips.core.PrintIterator;
import org.eclipse.nebula.paperclips.core.PrintPiece;
import org.eclipse.nebula.paperclips.core.internal.util.ResourcePool;
import org.eclipse.nebula.paperclips.core.internal.util.Util;
import org.eclipse.nebula.paperclips.core.text.internal.TextPiece;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
/**
* A Print for displaying text.
* <p>
* TextPrints are never greedy with layout space, even with center- or
* right-alignment. (Greedy prints take up all the available space on the page.)
* Therefore, when center- or right-alignment is required, it is necessary to
* wrap the text in a Print which will enforce the same alignment. Usually this
* is a center:default:grow or right:default:grow column in a GridPrint.
*
* @author Matthew Hall
*/
public class TextPrint implements Print {
/** The default text for a TextPrint. Value is "". */
public static final String DEFAULT_TEXT = ""; //$NON-NLS-1$
/** The default font data for a TextPrint. Value is device-dependent. */
public static final FontData DEFAULT_FONT_DATA = new FontData();
/** The default alignment for TextPrint. Value is SWT.LEFT. */
public static final int DEFAULT_ALIGN = SWT.LEFT;
private static final TextStyle DEFAULT_STYLE = new TextStyle();
String text;
TextStyle style;
boolean wordSplitting;
/**
* Constructs a TextPrint with the default properties.
*/
public TextPrint() {
this(DEFAULT_TEXT);
}
/**
* Constructs a TextPrint with the given text.
*
* @param text
* the text to print.
*/
public TextPrint(String text) {
this(text, DEFAULT_STYLE);
}
/**
* Constructs a TextPrint with the given text and font data.
*
* @param text
* the text to print.
* @param fontData
* the font that will be used to print the text.
*/
public TextPrint(String text, FontData fontData) {
this(text, DEFAULT_STYLE.font(fontData));
}
/**
* Constructs a TextPrint with the give text and alignment.
*
* @param text
* the text to print.
* @param align
* the horizontal text alignment. Must be one of {@link SWT#LEFT}
* , {@link SWT#CENTER} or {@link SWT#RIGHT}.
*/
public TextPrint(String text, int align) {
this(text, DEFAULT_STYLE.align(align));
}
/**
* Constructs a TextPrint with the given text, font data, and alignment.
*
* @param text
* the text to print.
* @param fontData
* the font that will be used to print the text.
* @param align
* the horizontal text alignment. Must be one of {@link SWT#LEFT}
* , {@link SWT#CENTER} or {@link SWT#RIGHT}.
*/
public TextPrint(String text, FontData fontData, int align) {
this(text, DEFAULT_STYLE.font(fontData).align(align));
}
/**
* Constructs a TextPrint with the given text and style.
*
* @param text
* the text to print.
* @param style
* the style to apply to the text.
*/
public TextPrint(String text, TextStyle style) {
Util.notNull(text, style);
this.text = text;
this.style = style;
this.wordSplitting = true;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((style == null) ? 0 : style.hashCode());
result = prime * result + ((text == null) ? 0 : text.hashCode());
result = prime * result + (wordSplitting ? 1231 : 1237);
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TextPrint other = (TextPrint) obj;
if (style == null) {
if (other.style != null)
return false;
} else if (!style.equals(other.style))
return false;
if (text == null) {
if (other.text != null)
return false;
} else if (!text.equals(other.text))
return false;
if (wordSplitting != other.wordSplitting)
return false;
return true;
}
/**
* Returns the text that will be printed.
*
* @return the text that will be printed.
*/
public String getText() {
return text;
}
/**
* Sets the text that will be printed.
*
* @param text
* the text to print.
*/
public void setText(String text) {
Util.notNull(text);
this.text = text;
}
/**
* Returns the text style.
*
* @return the text style.
*/
public TextStyle getStyle() {
return style;
}
/**
* Sets the text style to the argument.
*
* @param style
* the new text style.
*/
public void setStyle(TextStyle style) {
Util.notNull(style);
this.style = style;
}
/**
* Returns the font that will be used to print the text.
*
* @return the font that will be used to print the text.
*/
public FontData getFontData() {
return style.getFontData();
}
/**
* Sets the font that will be used to print the text.
*
* @param fontData
* the font that will be used to print the text.
*/
public void setFontData(FontData fontData) {
setStyle(style.font(fontData));
}
/**
* Returns the horizontal text alignment. Possible values include
* {@link SWT#LEFT}, {@link SWT#CENTER} or {@link SWT#RIGHT}.
*
* @return the horizontal text alignment.
*/
public int getAlignment() {
return style.getAlignment();
}
/**
* Sets the horizontal text alignment.
*
* @param alignment
* the horizontal text alignment. Must be one of {@link SWT#LEFT}
* , {@link SWT#CENTER} or {@link SWT#RIGHT}.
*/
public void setAlignment(int alignment) {
setStyle(style.align(alignment));
}
/**
* Returns the foreground color. A null value indicates that the foreground
* color is inherited.
*
* @return the foreground color.
*/
public RGB getForeground() {
return style.getForeground();
}
/**
* Sets the foreground color to the argument.
*
* @param foreground
* the new foreground color. A null value causes the foreground
* color to be inherited.
*/
public void setForeground(RGB foreground) {
setStyle(style.foreground(foreground));
}
/**
* Returns the background color. A null value indicates that the background
* is transparent.
*
* @return the background color.
*/
public RGB getBackground() {
return style.getBackground();
}
/**
* Sets the background color to the argument.
*
* @param background
* the new background color. A null value causes the background
* to be transparent.
*/
public void setBackground(RGB background) {
style = style.background(background);
}
/**
* Returns the underline flag.
*
* @return the underline flag.
*/
public boolean getUnderline() {
return style.getUnderline();
}
/**
* Sets the underline flag to the argument.
*
* @param underline
* the underline flag.
*/
public void setUnderline(boolean underline) {
style = style.underline(underline);
}
/**
* Returns the strikout flag.
*
* @return the strikout flag.
*/
public boolean getStrikeout() {
return style.getStrikeout();
}
/**
* Sets the strikeout flag to the argument.
*
* @param strikeout
* the strikeout flag.
*/
public void setStrikeout(boolean strikeout) {
style = style.strikeout(strikeout);
}
/**
* Returns whether word splitting is enabled. Default is true.
*
* @return whether word splitting is enabled.
*/
public boolean getWordSplitting() {
return wordSplitting;
}
/**
* Sets whether word splitting is enabled.
*
* @param wordBreaking
* whether to allow word splitting.
*/
public void setWordSplitting(boolean wordBreaking) {
this.wordSplitting = wordBreaking;
}
public PrintIterator iterator(Device device, GC gc) {
return new TextIterator(this, device, gc);
}
}
class TextIterator implements PrintIterator {
private final Device device;
private final GC gc;
final String text;
final String[] lines;
final TextStyle style;
final boolean wordSplitting;
final Point minimumSize;
final Point preferredSize;
int row;
int col;
TextIterator(TextPrint print, Device device, GC gc) {
this.device = device;
this.gc = gc;
this.text = print.text;
this.lines = print.text.split("(\r)?\n"); //$NON-NLS-1$
this.style = print.style;
this.wordSplitting = print.wordSplitting;
this.minimumSize = maxExtent(text.split("\\s")); //$NON-NLS-1$
this.preferredSize = maxExtent(lines);
this.row = 0;
this.col = 0;
}
TextIterator(TextIterator that) {
this.device = that.device;
this.gc = that.gc;
this.text = that.text;
this.lines = that.lines;
this.style = that.style;
this.wordSplitting = that.wordSplitting;
this.minimumSize = that.minimumSize;
this.preferredSize = that.preferredSize;
this.row = that.row;
this.col = that.col;
}
public boolean hasNext() {
return row < lines.length;
}
public PrintPiece next(int width, int height) {
if (!hasNext())
PaperClips.error("No more content."); //$NON-NLS-1$
Font oldFont = initGC();
PrintPiece result = internalNext(width, height);
restoreGC(oldFont);
return result;
}
private PrintPiece internalNext(int width, int height) {
FontMetrics fm = gc.getFontMetrics();
final int lineHeight = fm.getHeight();
if (height < lineHeight)
return null;
final int maxLines = height / lineHeight;
String[] nextLines = nextLines(width, maxLines);
if (nextLines.length == 0)
return null;
int maxWidth = maxExtent(nextLines).x;
Point size = new Point(maxWidth, nextLines.length * lineHeight);
int ascent = fm.getAscent() + fm.getLeading();
return new TextPiece(device, style, nextLines, size, ascent);
}
private Font initGC() {
Font oldFont = gc.getFont();
FontData fontData = style.getFontData();
if (fontData != null)
gc.setFont(ResourcePool.forDevice(device).getFont(fontData));
return oldFont;
}
private void restoreGC(Font oldFont) {
gc.setFont(oldFont);
}
private String[] nextLines(final int width, final int maxLines) {
List nextLines = new ArrayList(Math.min(lines.length, maxLines));
while ((nextLines.size() < maxLines) && (row < lines.length)) {
String line = lines[row].substring(col);
// Find out how much text will fit on one line.
int charCount = findLineBreak(gc, line, width);
// If none of the text could fit in the current line, terminate this
// iteration.
if (line.length() > 0 && charCount == 0)
break;
// Get the text that fits on this line.
String thisLine = line.substring(0, charCount);
nextLines.add(thisLine);
// Move cursor past the text we just consumed.
col += charCount;
skipWhitespace();
advanceToNextRowIfCurrentRowCompleted();
}
return (String[]) nextLines.toArray(new String[nextLines.size()]);
}
private void skipWhitespace() {
while (col < lines[row].length()
&& Character.isWhitespace(lines[row].charAt(col)))
col++;
}
private void advanceToNextRowIfCurrentRowCompleted() {
if (col >= lines[row].length()) {
row++;
col = 0;
}
}
public Point minimumSize() {
return new Point(minimumSize.x, minimumSize.y);
}
public Point preferredSize() {
return new Point(preferredSize.x, preferredSize.y);
}
private Point maxExtent(String[] text) {
Font oldFont = gc.getFont();
try {
initGC();
FontMetrics fm = gc.getFontMetrics();
int maxWidth = 0;
for (int i = 0; i < text.length; i++) {
String textPiece = text[i];
maxWidth = Math.max(maxWidth, gc.stringExtent(textPiece).x);
}
return new Point(maxWidth, fm.getHeight());
} finally {
restoreGC(oldFont);
}
}
private int findLineBreak(GC gc, String text, int width) {
// Offsets within the string
int loIndex = 0;
int hiIndex = text.length();
// Pixel width of entire string
int pixelWidth = gc.stringExtent(text).x;
// Does the whole string fit?
if (pixelWidth <= width)
// I'll take it
return hiIndex;
// Do a binary search to find the maximum characters that will fit
// within the given width.
while (loIndex < hiIndex) {
int midIndex = (loIndex + hiIndex + 1) / 2;
int midWidth = gc.stringExtent(text.substring(0, midIndex)).x;
if (midWidth < width)
// don't add 1, the next character could make it too big
loIndex = midIndex;
else if (midWidth > width)
// subtract 1, we already know midIndex makes it too big
hiIndex = midIndex - 1;
else {
// perfect fit
loIndex = hiIndex = midIndex;
}
}
return findWordBreak(text, loIndex);
}
int findWordBreak(String text, int maxLength) {
// If the max length is the string length, no break
// (we mainly check this to avoid an exception in for-loop)
if (maxLength == text.length())
return maxLength;
// Otherwise, break string at the last whitespace at or before
// maxLength.
for (int i = maxLength; i >= 0; i--)
if (Character.isWhitespace(text.charAt(i)))
return i;
// No whitespace? Break at max length (if word breaking is allowed)
if (wordSplitting)
return maxLength;
return 0;
}
public PrintIterator copy() {
return new TextIterator(this);
}
}