blob: a42acb8e6f24034d182479a31510cadb7e24ba7e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2010 IBM Corporation 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.draw2d.text;
import java.util.List;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.TextUtilities;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.rap.swt.SWT;
/**
* An inline flow figure that renders a string of text across one or more lines.
* A TextFlow cannot contain children. All <code>InlineFlow</code> figure's must
* be parented by a <code>FlowFigure</code>.
* <p>
* WARNING: This class is not intended to be subclassed by clients.
*
* @author hudsonr
* @author Pratik Shah
* @since 2.1
*/
public class TextFlow extends InlineFlow {
static final String ELLIPSIS = "..."; //$NON-NLS-1$
private BidiInfo bidiInfo;
private int selectionEnd = -1;
private String text;
/**
* Constructs a new TextFlow with the empty String.
*
* @see java.lang.Object#Object()
*/
public TextFlow() {
this(new String());
}
/**
* Constructs a new TextFlow with the specified String.
*
* @param s
* the string
*/
public TextFlow(String s) {
text = s;
}
/**
* Returns the width of the text until the first line-break.
*
* @see org.eclipse.draw2d.text.FlowFigure#addLeadingWordRequirements(int[])
*/
public boolean addLeadingWordRequirements(int[] width) {
return addLeadingWordWidth(getText(), width);
}
/**
* Calculates the width taken up by the given text before a line-break is
* encountered.
*
* @param text
* the text in which the break is to be found
* @param width
* the width before the next line-break (if one's found; the
* width of all the given text, otherwise) will be added on to
* the first int in the given array
* @return <code>true</code> if a line-break was found
* @since 3.1
*/
boolean addLeadingWordWidth(String text, int[] width) {
if (text.length() == 0)
return false;
if (Character.isWhitespace(text.charAt(0)))
return true;
text = 'a' + text + 'a';
FlowUtilities.LINE_BREAK.setText(text);
int index = FlowUtilities.LINE_BREAK.next() - 1;
if (index == 0)
return true;
while (Character.isWhitespace(text.charAt(index)))
index--;
boolean result = index < text.length() - 1;
// index should point to the end of the actual text (not including the
// 'a' that was
// appended), if there were no breaks
if (index == text.length() - 1)
index--;
text = text.substring(1, index + 1);
if (bidiInfo == null)
width[0] += getTextUtilities().getTextExtents(text, getFont()).width;
else {
TextLayout textLayout = FlowUtilities.getTextLayout();
textLayout.setFont(getFont());
textLayout.setText(text);
width[0] += textLayout.getBounds().width;
}
return result;
}
/**
* A TextFlow contributes its text.
*
* @see org.eclipse.draw2d.text.FlowFigure#contributeBidi(org.eclipse.draw2d.text.BidiProcessor)
*/
protected void contributeBidi(BidiProcessor proc) {
bidiInfo = null;
proc.add(this, getText());
}
/**
* @see org.eclipse.draw2d.text.InlineFlow#createDefaultFlowLayout()
*/
protected FlowFigureLayout createDefaultFlowLayout() {
return new ParagraphTextLayout(this);
}
private int findNextLineOffset(Point p, int[] trailing) {
if (getBounds().bottom() <= p.y)
return -1;
TextFragmentBox closestBox = null;
int index = 0;
List fragments = getFragmentsWithoutBorder();
for (int i = fragments.size() - 1; i >= 0; i--) {
TextFragmentBox box = (TextFragmentBox) fragments.get(i);
if (box.getBaseline() - box.getLineRoot().getAscent() > p.y
&& (closestBox == null
|| box.getBaseline() < closestBox.getBaseline() || (box
.getBaseline() == closestBox.getBaseline() && hDistanceBetween(
box, p.x) < hDistanceBetween(closestBox, p.x)))) {
closestBox = box;
index = i;
}
}
return findOffset(p, trailing, closestBox, index);
}
private int findOffset(Point p, int[] trailing, TextFragmentBox box,
int boxIndex) {
if (box == null)
return -1;
TextLayout layout = FlowUtilities.getTextLayout();
layout.setFont(getFont());
layout.setText(getBidiSubstring(box, boxIndex));
int x = p.x - box.getX();
if (isMirrored())
x = box.getWidth() - x;
int layoutOffset = layout
.getOffset(x, p.y - box.getTextTop(), trailing);
return box.offset + layoutOffset - getBidiPrefixLength(box, boxIndex);
}
private int findPreviousLineOffset(Point p, int[] trailing) {
if (getBounds().y > p.y)
return -1;
TextFragmentBox closestBox = null;
int index = 0;
List fragments = getFragmentsWithoutBorder();
for (int i = fragments.size() - 1; i >= 0; i--) {
TextFragmentBox box = (TextFragmentBox) fragments.get(i);
if (box.getBaseline() + box.getLineRoot().getDescent() < p.y
&& (closestBox == null
|| box.getBaseline() > closestBox.getBaseline() || (box
.getBaseline() == closestBox.getBaseline() && hDistanceBetween(
box, p.x) < hDistanceBetween(closestBox, p.x)))) {
closestBox = box;
index = i;
}
}
return findOffset(p, trailing, closestBox, index);
}
int getAscent() {
return getTextUtilities().getAscent(getFont());
}
/**
* Returns the BidiInfo for this figure or <code>null</code>.
*
* @return <code>null</code> or the info
* @since 3.1
*/
public BidiInfo getBidiInfo() {
return bidiInfo;
}
private int getBidiPrefixLength(TextFragmentBox box, int index) {
if (box.getBidiLevel() < 1)
return 0;
if (index > 0 || !bidiInfo.leadingJoiner)
return 1;
return 2;
}
/**
* @param box
* which fragment
* @param index
* the fragment index
* @return the bidi string for that fragment
* @since 3.1
*/
protected String getBidiSubstring(TextFragmentBox box, int index) {
if (box.getBidiLevel() < 1)
return getText().substring(box.offset, box.offset + box.length);
StringBuffer buffer = new StringBuffer(box.length + 3);
buffer.append(box.isRightToLeft() ? BidiChars.RLO : BidiChars.LRO);
if (index == 0 && bidiInfo.leadingJoiner)
buffer.append(BidiChars.ZWJ);
buffer.append(getText().substring(box.offset, box.offset + box.length));
if (index == getFragmentsWithoutBorder().size() - 1
&& bidiInfo.trailingJoiner)
buffer.append(BidiChars.ZWJ);
return buffer.toString();
}
/**
* Returns the CaretInfo in absolute coordinates. The offset must be between
* 0 and the length of the String being displayed.
*
* @since 3.1
* @param offset
* the location in this figure's text
* @param trailing
* true if the caret is being placed after the offset
* @exception IllegalArgumentException
* If the offset is not between <code>0</code> and the length
* of the string inclusively
* @return the caret bounds relative to this figure
*/
public CaretInfo getCaretPlacement(int offset, boolean trailing) {
if (offset < 0 || offset > getText().length())
throw new IllegalArgumentException("Offset: " + offset //$NON-NLS-1$
+ " is invalid"); //$NON-NLS-1$
if (offset == getText().length())
trailing = false;
List fragments = getFragmentsWithoutBorder();
int i = fragments.size();
TextFragmentBox box;
do
box = (TextFragmentBox) fragments.get(--i);
while (offset < box.offset && i > 0);
// Cannot be trailing and after the last char, so go to first char in
// next box
if (trailing && box.offset + box.length <= offset) {
box = (TextFragmentBox) fragments.get(++i);
offset = box.offset;
trailing = false;
}
Point where = getPointInBox(box, offset, i, trailing);
CaretInfo info = new CaretInfo(where.x, where.y, box.getAscent(),
box.getDescent(), box.getLineRoot().getAscent(), box
.getLineRoot().getDescent());
translateToAbsolute(info);
return info;
}
Point getPointInBox(TextFragmentBox box, int offset, int index,
boolean trailing) {
offset -= box.offset;
offset = Math.min(box.length, offset);
Point result = new Point(0, box.getTextTop());
if (bidiInfo == null) {
if (trailing && offset < box.length)
offset++;
String substring = getText().substring(box.offset,
box.offset + offset);
result.x = getTextUtilities().getTextExtents(substring, getFont()).width;
} else {
TextLayout layout = FlowUtilities.getTextLayout();
layout.setFont(getFont());
String fragString = getBidiSubstring(box, index);
layout.setText(fragString);
offset += getBidiPrefixLength(box, index);
result.x = layout.getLocation(offset, trailing).x;
if (isMirrored())
result.x = box.width - result.x;
}
result.x += box.getX();
return result;
}
int getDescent() {
return getTextUtilities().getDescent(getFont());
}
/**
* Returns the minimum character offset which is on the given baseline
* y-coordinate. The y location should be relative to this figure. The
* return value will be between 0 and N-1. If no fragment is located on the
* baseline, <code>-1</code> is returned.
*
* @since 3.1
* @param baseline
* the relative baseline coordinate
* @return -1 or the lowest offset for the line
*/
public int getFirstOffsetForLine(int baseline) {
TextFragmentBox box;
List fragments = getFragmentsWithoutBorder();
for (int i = 0; i < fragments.size(); i++) {
box = (TextFragmentBox) fragments.get(i);
if (baseline == box.getBaseline())
return box.offset;
}
return -1;
}
/**
* Returns the <code>TextFragmentBox</code> fragments contained in this
* TextFlow, not including the border fragments. The returned list should
* not be modified.
*
* @return list of fragments without the border fragments
* @since 3.4
*/
protected List getFragmentsWithoutBorder() {
List fragments = getFragments();
if (getBorder() != null)
fragments = fragments.subList(1, fragments.size() - 1);
return fragments;
}
/**
* Returns the maximum offset for a character which is on the given baseline
* y-coordinate. The y location should be relative to this figure. The
* return value will be between 0 and N-1. If no fragment is located on the
* baseline, <code>-1</code> is returned.
*
* @since 3.1
* @param baseline
* the relative baseline coordinate
* @return -1 or the highest offset at the given baseline
*/
public int getLastOffsetForLine(int baseline) {
TextFragmentBox box;
List fragments = getFragmentsWithoutBorder();
for (int i = fragments.size() - 1; i >= 0; i--) {
box = (TextFragmentBox) fragments.get(i);
if (baseline == box.getBaseline())
return box.offset + box.length - 1;
}
return -1;
}
/**
* Returns the offset nearest the given point either up or down one line. If
* no offset is found, -1 is returned. <code>trailing[0]</code> will be set
* to 1 if the reference point is closer to the trailing edge of the offset
* than it is to the leading edge.
*
* @since 3.1
* @param p
* a reference point
* @param down
* <code>true</code> if the search is down
* @param trailing
* an int array
* @return the next offset or <code>-1</code>
*/
public int getNextOffset(Point p, boolean down, int[] trailing) {
return down ? findNextLineOffset(p, trailing) : findPreviousLineOffset(
p, trailing);
}
/**
* Returns the next offset which is visible in at least one fragment or -1
* if there is not one. A visible offset means that the character or the one
* preceding it is displayed, which implies that a caret can be positioned
* at such an offset. This is useful for advancing a caret past characters
* which resulted in a line wrap.
*
* @param offset
* the reference offset
* @return the next offset which is visible
* @since 3.1
*/
public int getNextVisibleOffset(int offset) {
TextFragmentBox box;
List fragments = getFragmentsWithoutBorder();
for (int i = 0; i < fragments.size(); i++) {
box = (TextFragmentBox) fragments.get(i);
if (box.offset + box.length <= offset)
continue;
return Math.max(box.offset, offset + 1);
}
return -1;
}
/**
* Returns the offset of the character directly below or nearest the given
* location. The point must be relative to this figure. The return value
* will be between 0 and N-1. If the proximity argument is not
* <code>null</code>, the result may also be <code>-1</code> if no offset
* was found within the proximity.
* <P>
* For a typical character, the trailing argument will be filled in to
* indicate whether the point is closer to the leading edge (0) or the
* trailing edge (1). When the point is over a cluster composed of multiple
* characters, the trailing argument will be filled with the position of the
* character in the cluster that is closest to the point.
* <P>
* If the proximity argument is not <code>null</code>, then the location may
* be no further than the proximity given. Passing <code>null</code> is
* equivalent to passing <code>new
* Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)</code>. The
* <code>width</code> field of the proximity will contain the horizontal
* distance, <code>height</code> will contain vertical. Vertical proximity
* is more important than horizontal. The returned offset is the lowest
* index with minimum vertical proximity not exceeding the given limit, with
* horizontal proximity not exceeding the given limit. If an offset that is
* within the proximity is found, then the given <code>Dimension</code> will
* be updated to reflect the new proximity.
*
*
* @since 3.1
* @param p
* the point relative to this figure
* @param trailing
* the trailing buffer
* @param proximity
* restricts and records the distance of the returned offset
* @return the nearest offset in this figure's text
*/
public int getOffset(Point p, int trailing[], Dimension proximity) {
if (proximity == null)
proximity = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
TextFragmentBox closestBox = null;
int index = 0;
int dy;
int dx;
int i = 0;
int size = fragments.size();
if (getBorder() instanceof FlowBorder) {
i++;
size--;
}
for (; i < size; i++) {
TextFragmentBox box = (TextFragmentBox) fragments.get(i);
dy = vDistanceBetween(box, p.y);
if (dy > proximity.height)
continue;
dx = hDistanceBetween(box, p.x);
if (dy == proximity.height && dx >= proximity.width)
continue;
proximity.height = dy;
proximity.width = dx;
closestBox = box;
index = i;
}
return findOffset(p, trailing, closestBox, index);
}
/**
* Returns the previous offset which is visible in at least one fragment or
* -1 if there is not one. See {@link #getNextVisibleOffset(int)} for more.
*
* @param offset
* a reference offset
* @return -1 or the previous offset which is visible
* @since 3.1
*/
public int getPreviousVisibleOffset(int offset) {
TextFragmentBox box;
if (offset == -1)
offset = Integer.MAX_VALUE;
List fragments = getFragmentsWithoutBorder();
for (int i = fragments.size() - 1; i >= 0; i--) {
box = (TextFragmentBox) fragments.get(i);
if (box.offset >= offset)
continue;
return Math.min(box.offset + box.length, offset - 1);
}
return -1;
}
/**
* @return the String being displayed; will not be <code>null</code>
*/
public String getText() {
return text;
}
int getVisibleAscent() {
if (getBorder() instanceof FlowBorder) {
FlowBorder border = (FlowBorder) getBorder();
return border.getInsets(this).top + getAscent();
}
return getAscent();
}
int getVisibleDescent() {
if (getBorder() instanceof FlowBorder) {
FlowBorder border = (FlowBorder) getBorder();
return border.getInsets(this).bottom + getDescent();
}
return getDescent();
}
private int hDistanceBetween(TextFragmentBox box, int x) {
if (x < box.getX())
return box.getX() - x;
return Math.max(0, x - (box.getX() + box.getWidth()));
}
/**
* Returns <code>true</code> if a portion if the text is truncated using
* ellipses ("...").
*
* @return <code>true</code> if the text is truncated with ellipses
*/
public boolean isTextTruncated() {
for (int i = 0; i < fragments.size(); i++) {
if (((TextFragmentBox) fragments.get(i)).isTruncated())
return true;
}
return false;
}
/**
* @see org.eclipse.draw2d.Figure#paintFigure(Graphics)
*/
protected void paintFigure(Graphics g) {
TextFragmentBox frag;
g.getClip(Rectangle.SINGLETON);
int yStart = Rectangle.SINGLETON.y;
int yEnd = Rectangle.SINGLETON.bottom();
for (int i = 0; i < fragments.size(); i++) {
frag = (TextFragmentBox) fragments.get(i);
// g.drawLine(frag.getX(), frag.getLineRoot().getVisibleTop(),
// frag.getWidth() + frag.getX(),
// frag.getLineRoot().getVisibleTop());
// g.drawLine(frag.getX(), frag.getBaseline(), frag.getWidth() +
// frag.getX(), frag.getBaseline());
if (frag.offset == -1)
continue;
// Loop until first visible fragment
if (yStart > frag.getLineRoot().getVisibleBottom() + 1)// The + 1 is
// for
// disabled
// text
continue;
// Break loop at first non-visible fragment
if (yEnd < frag.getLineRoot().getVisibleTop())
break;
String draw = getBidiSubstring(frag, i);
if (frag.isTruncated())
draw += ELLIPSIS;
if (!isEnabled()) {
Color fgColor = g.getForegroundColor();
g.setForegroundColor(ColorConstants.buttonLightest);
paintText(g, draw, frag.getX() + 1, frag.getBaseline()
- getAscent() + 1, frag.getBidiLevel());
g.setForegroundColor(ColorConstants.buttonDarker);
paintText(g, draw, frag.getX(), frag.getBaseline()
- getAscent(), frag.getBidiLevel());
g.setForegroundColor(fgColor);
} else {
paintText(g, draw, frag.getX(), frag.getBaseline()
- getAscent(), frag.getBidiLevel());
}
}
}
/**
* @see InlineFlow#paintSelection(org.eclipse.draw2d.Graphics)
*/
protected void paintSelection(Graphics graphics) {
if (selectionStart == -1)
return;
graphics.setXORMode(true);
graphics.setBackgroundColor(ColorConstants.white);
TextFragmentBox frag;
for (int i = 0; i < fragments.size(); i++) {
frag = (TextFragmentBox) fragments.get(i);
// Loop until first visible fragment
if (frag.offset + frag.length <= selectionStart)
continue;
if (frag.offset > selectionEnd)
return;
if (selectionStart <= frag.offset
&& selectionEnd >= frag.offset + frag.length) {
int y = frag.getLineRoot().getVisibleTop();
int height = frag.getLineRoot().getVisibleBottom() - y;
graphics.fillRectangle(frag.getX(), y, frag.getWidth(), height);
} else if (selectionEnd > frag.offset
&& selectionStart < frag.offset + frag.length) {
Point p1 = getPointInBox(frag,
Math.max(frag.offset, selectionStart), i, false);
Point p2 = getPointInBox(frag,
Math.min(frag.offset + frag.length, selectionEnd) - 1,
i, true);
Rectangle rect = new Rectangle(p1, p2);
rect.width--;
rect.y = frag.getLineRoot().getVisibleTop();
rect.height = frag.getLineRoot().getVisibleBottom() - rect.y;
graphics.fillRectangle(rect);
}
}
}
protected void paintText(Graphics g, String draw, int x, int y,
int bidiLevel) {
if (bidiLevel == -1) {
g.drawText(draw, x, y);
} else {
TextLayout tl = FlowUtilities.getTextLayout();
if (isMirrored())
tl.setOrientation(SWT.RIGHT_TO_LEFT);
tl.setFont(g.getFont());
tl.setText(draw);
g.drawTextLayout(tl, x, y);
}
}
/**
* @see org.eclipse.draw2d.text.FlowFigure#setBidiInfo(org.eclipse.draw2d.text.BidiInfo)
*/
public void setBidiInfo(BidiInfo info) {
this.bidiInfo = info;
}
/**
* Sets the extent of selection. The selection range is inclusive. For
* example, the range [0, 0] indicates that the first character is selected.
*
* @param start
* the start offset
* @param end
* the end offset
* @since 3.1
*/
public void setSelection(int start, int end) {
boolean repaint = false;
if (selectionStart == start) {
if (selectionEnd == end)
return;
repaint = true;
} else
repaint = selectionStart != selectionEnd || start != end;
selectionStart = start;
selectionEnd = end;
if (repaint)
repaint();
}
/**
* Sets the text being displayed. The string may not be <code>null</code>.
*
* @param s
* The new text
*/
public void setText(String s) {
if (s != null && !s.equals(text)) {
text = s;
revalidateBidi(this);
repaint();
}
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return text;
}
private int vDistanceBetween(TextFragmentBox box, int y) {
int top = box.getBaseline() - box.getLineRoot().getAscent();
if (y < top)
return top - y;
return Math.max(0, y
- (box.getBaseline() + box.getLineRoot().getDescent()));
}
/**
* Gets the <code>FlowUtilities</code> instance to be used in measurement
* calculations.
*
* @return a <code>FlowUtilities</code> instance
* @since 3.4
*/
protected FlowUtilities getFlowUtilities() {
return FlowUtilities.INSTANCE;
}
/**
* Gets the <code>TextUtilities</code> instance to be used in measurement
* calculations.
*
* @return a <code>TextUtilities</code> instance
* @since 3.4
*/
protected TextUtilities getTextUtilities() {
return TextUtilities.INSTANCE;
}
}