/******************************************************************************* * 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.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.INSTANCE().LINE_BREAK.setText(text); int index = FlowUtilities.INSTANCE().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.INSTANCE().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.INSTANCE().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.INSTANCE().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.getSINGLETON()); int yStart = Rectangle.getSINGLETON().y; int yEnd = Rectangle.getSINGLETON().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.INSTANCE().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(); } }