| /******************************************************************************* |
| * 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 com.ibm.icu.text.BreakIterator; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.graphics.TextLayout; |
| import org.eclipse.swt.widgets.Display; |
| |
| import org.eclipse.draw2d.FigureUtilities; |
| import org.eclipse.draw2d.TextUtilities; |
| |
| /** |
| * Utility class for FlowFigures. |
| * |
| * @author hudsonr |
| * @since 3.4 |
| */ |
| public class FlowUtilities { |
| |
| interface LookAhead { |
| int getWidth(); |
| } |
| |
| /** |
| * a singleton default instance |
| */ |
| public static FlowUtilities INSTANCE = new FlowUtilities(); |
| |
| private static final BreakIterator INTERNAL_LINE_BREAK = BreakIterator |
| .getLineInstance(); |
| private static TextLayout layout; |
| |
| static final BreakIterator LINE_BREAK = BreakIterator.getLineInstance(); |
| |
| static boolean canBreakAfter(char c) { |
| boolean result = Character.isWhitespace(c) || c == '-'; |
| if (!result && (c < 'a' || c > 'z')) { |
| // chinese characters and such would be caught in here |
| // LINE_BREAK is used here because INTERNAL_LINE_BREAK might be in |
| // use |
| LINE_BREAK.setText(c + "a"); //$NON-NLS-1$ |
| result = LINE_BREAK.isBoundary(1); |
| } |
| return result; |
| } |
| |
| private static int findFirstDelimeter(String string) { |
| int macNL = string.indexOf('\r'); |
| int unixNL = string.indexOf('\n'); |
| |
| if (macNL == -1) |
| macNL = Integer.MAX_VALUE; |
| if (unixNL == -1) |
| unixNL = Integer.MAX_VALUE; |
| |
| return Math.min(macNL, unixNL); |
| } |
| |
| /** |
| * Gets the average character width. |
| * |
| * @param fragment |
| * the supplied TextFragmentBox to use for calculation. if the |
| * length is 0 or if the width is or below 0, the average |
| * character width is taken from standard font metrics. |
| * @param font |
| * the font to use in case the TextFragmentBox conditions above |
| * are true. |
| * @return the average character width |
| */ |
| protected float getAverageCharWidth(TextFragmentBox fragment, Font font) { |
| if (fragment.getWidth() > 0 && fragment.length != 0) |
| return fragment.getWidth() / (float) fragment.length; |
| return FigureUtilities.getFontMetrics(font).getAverageCharWidth(); |
| } |
| |
| static int getBorderAscent(InlineFlow owner) { |
| if (owner.getBorder() instanceof FlowBorder) { |
| FlowBorder border = (FlowBorder) owner.getBorder(); |
| return border.getInsets(owner).top; |
| } |
| return 0; |
| } |
| |
| static int getBorderAscentWithMargin(InlineFlow owner) { |
| if (owner.getBorder() instanceof FlowBorder) { |
| FlowBorder border = (FlowBorder) owner.getBorder(); |
| return border.getTopMargin() + border.getInsets(owner).top; |
| } |
| return 0; |
| } |
| |
| static int getBorderDescent(InlineFlow owner) { |
| if (owner.getBorder() instanceof FlowBorder) { |
| FlowBorder border = (FlowBorder) owner.getBorder(); |
| return border.getInsets(owner).bottom; |
| } |
| return 0; |
| } |
| |
| static int getBorderDescentWithMargin(InlineFlow owner) { |
| if (owner.getBorder() instanceof FlowBorder) { |
| FlowBorder border = (FlowBorder) owner.getBorder(); |
| return border.getBottomMargin() + border.getInsets(owner).bottom; |
| } |
| return 0; |
| } |
| |
| /** |
| * Provides a TextLayout that can be used by the Draw2d text package for |
| * Bidi. This TextLayout should not be disposed by clients. The provided |
| * TextLayout's orientation will be LTR. |
| * |
| * @return an SWT TextLayout that can be used for Bidi |
| * @since 3.1 |
| */ |
| static TextLayout getTextLayout() { |
| if (layout == null) |
| layout = new TextLayout(Display.getDefault()); |
| layout.setOrientation(SWT.LEFT_TO_RIGHT); |
| return layout; |
| } |
| |
| /** |
| * @param frag |
| * @param string |
| * @param font |
| * @since 3.1 |
| */ |
| private static void initBidi(TextFragmentBox frag, String string, Font font) { |
| if (frag.requiresBidi()) { |
| TextLayout textLayout = getTextLayout(); |
| textLayout.setFont(font); |
| // $TODO need to insert overrides in front of string. |
| textLayout.setText(string); |
| } |
| } |
| |
| private int measureString(TextFragmentBox frag, String string, int guess, |
| Font font) { |
| if (frag.requiresBidi()) { |
| // The text and/or could have changed if the lookAhead was invoked. |
| // This will |
| // happen at most once. |
| return getTextLayoutBounds(string, font, 0, guess - 1).width; |
| } else |
| return getTextUtilities().getTextExtents( |
| string.substring(0, guess), font).width; |
| } |
| |
| /** |
| * Sets up the fragment width based using the font and string passed in. |
| * |
| * @param fragment |
| * the text fragment whose width will be set |
| * @param font |
| * the font to be used in the calculation |
| * @param string |
| * the string to be used in the calculation |
| */ |
| final protected void setupFragment(TextFragmentBox fragment, Font font, |
| String string) { |
| if (fragment.getWidth() == -1 || fragment.isTruncated()) { |
| int width; |
| if (string.length() == 0 || fragment.length == 0) |
| width = 0; |
| else if (fragment.requiresBidi()) { |
| width = getTextLayoutBounds(string, font, 0, |
| fragment.length - 1).width; |
| } else |
| width = getTextUtilities().getTextExtents( |
| string.substring(0, fragment.length), font).width; |
| if (fragment.isTruncated()) |
| width += getEllipsisWidth(font); |
| fragment.setWidth(width); |
| } |
| } |
| |
| /** |
| * Sets up a fragment and returns the number of characters consumed from the |
| * given String. An average character width can be provided as a hint for |
| * faster calculation. If a fragment's bidi level is set, a TextLayout will |
| * be used to calculate the width. |
| * |
| * @param frag |
| * the TextFragmentBox |
| * @param string |
| * the String |
| * @param font |
| * the Font used for measuring |
| * @param context |
| * the flow context |
| * @param wrapping |
| * the word wrap style |
| * @return the number of characters that will fit in the given space; can be |
| * 0 (eg., when the first character of the given string is a |
| * newline) |
| */ |
| final protected int wrapFragmentInContext(TextFragmentBox frag, |
| String string, FlowContext context, LookAhead lookahead, Font font, |
| int wrapping) { |
| frag.setTruncated(false); |
| int strLen = string.length(); |
| if (strLen == 0) { |
| frag.setWidth(-1); |
| frag.length = 0; |
| setupFragment(frag, font, string); |
| context.addToCurrentLine(frag); |
| return 0; |
| } |
| |
| INTERNAL_LINE_BREAK.setText(string); |
| |
| initBidi(frag, string, font); |
| float avgCharWidth = getAverageCharWidth(frag, font); |
| frag.setWidth(-1); |
| |
| /* |
| * Setup initial boundaries within the string. |
| */ |
| int absoluteMin = 0; |
| int max, min = 1; |
| if (wrapping == ParagraphTextLayout.WORD_WRAP_HARD) { |
| absoluteMin = INTERNAL_LINE_BREAK.next(); |
| while (absoluteMin > 0 |
| && Character.isWhitespace(string.charAt(absoluteMin - 1))) |
| absoluteMin--; |
| min = Math.max(absoluteMin, 1); |
| } |
| int firstDelimiter = findFirstDelimeter(string); |
| if (firstDelimiter == 0) |
| min = max = 0; |
| else |
| max = Math.min(strLen, firstDelimiter) + 1; |
| |
| int availableWidth = context.getRemainingLineWidth(); |
| int guess = 0, guessSize = 0; |
| |
| while (true) { |
| if ((max - min) <= 1) { |
| if (min == absoluteMin |
| && context.isCurrentLineOccupied() |
| && !context.getContinueOnSameLine() |
| && availableWidth < measureString(frag, string, min, |
| font) |
| + ((min == strLen && lookahead != null) ? lookahead |
| .getWidth() : 0)) { |
| context.endLine(); |
| availableWidth = context.getRemainingLineWidth(); |
| max = Math.min(strLen, firstDelimiter) + 1; |
| if ((max - min) <= 1) |
| break; |
| } else |
| break; |
| } |
| // Pick a new guess size |
| // New guess is the last guess plus the missing width in pixels |
| // divided by the average character size in pixels |
| guess += 0.5f + (availableWidth - guessSize) / avgCharWidth; |
| |
| if (guess >= max) |
| guess = max - 1; |
| if (guess <= min) |
| guess = min + 1; |
| |
| guessSize = measureString(frag, string, guess, font); |
| |
| if (guess == strLen && lookahead != null |
| && !canBreakAfter(string.charAt(strLen - 1)) |
| && guessSize + lookahead.getWidth() > availableWidth) { |
| max = guess; |
| continue; |
| } |
| |
| if (guessSize <= availableWidth) { |
| min = guess; |
| frag.setWidth(guessSize); |
| if (guessSize == availableWidth) |
| max = guess + 1; |
| } else |
| max = guess; |
| } |
| |
| int result = min; |
| boolean continueOnLine = false; |
| if (min == strLen) { |
| // Everything fits |
| if (string.charAt(strLen - 1) == ' ') { |
| if (frag.getWidth() == -1) { |
| frag.length = result; |
| frag.setWidth(measureString(frag, string, result, font)); |
| } |
| if (lookahead.getWidth() > availableWidth - frag.getWidth()) { |
| frag.length = result - 1; |
| frag.setWidth(-1); |
| } else |
| frag.length = result; |
| } else { |
| continueOnLine = !canBreakAfter(string.charAt(strLen - 1)); |
| frag.length = result; |
| } |
| } else if (min == firstDelimiter) { |
| // move result past the delimiter |
| frag.length = result; |
| if (string.charAt(min) == '\r') { |
| result++; |
| if (++min < strLen && string.charAt(min) == '\n') |
| result++; |
| } else if (string.charAt(min) == '\n') |
| result++; |
| } else if (string.charAt(min) == ' ' |
| || canBreakAfter(string.charAt(min - 1)) |
| || INTERNAL_LINE_BREAK.isBoundary(min)) { |
| frag.length = min; |
| if (string.charAt(min) == ' ') |
| result++; |
| else if (string.charAt(min - 1) == ' ') { |
| frag.length--; |
| frag.setWidth(-1); |
| } |
| } else |
| out: { |
| // In the middle of an unbreakable offset |
| result = INTERNAL_LINE_BREAK.preceding(min); |
| if (result == 0) { |
| switch (wrapping) { |
| case ParagraphTextLayout.WORD_WRAP_TRUNCATE: |
| int truncatedWidth = availableWidth |
| - getEllipsisWidth(font); |
| if (truncatedWidth > 0) { |
| // $TODO this is very slow. It should be using |
| // avgCharWidth to go faster |
| while (min > 0) { |
| guessSize = measureString(frag, string, min, |
| font); |
| if (guessSize <= truncatedWidth) |
| break; |
| min--; |
| } |
| frag.length = min; |
| } else |
| frag.length = 0; |
| frag.setTruncated(true); |
| result = INTERNAL_LINE_BREAK.following(max - 1); |
| break out; |
| |
| default: |
| result = min; |
| break; |
| } |
| } |
| frag.length = result; |
| if (string.charAt(result - 1) == ' ') |
| frag.length--; |
| frag.setWidth(-1); |
| } |
| |
| setupFragment(frag, font, string); |
| context.addToCurrentLine(frag); |
| context.setContinueOnSameLine(continueOnLine); |
| return result; |
| } |
| |
| /** |
| * @see TextLayout#getBounds() |
| */ |
| protected Rectangle getTextLayoutBounds(String s, Font f, int start, int end) { |
| TextLayout textLayout = getTextLayout(); |
| textLayout.setFont(f); |
| textLayout.setText(s); |
| return textLayout.getBounds(start, end); |
| } |
| |
| /** |
| * Returns an instance of a <code>TextUtililities</code> class on which text |
| * calculations can be performed. Clients may override to customize. |
| * |
| * @return the <code>TextUtililities</code> instance |
| * @since 3.4 |
| */ |
| protected TextUtilities getTextUtilities() { |
| return TextUtilities.INSTANCE; |
| } |
| |
| /** |
| * Gets the ellipsis width. |
| * |
| * @param font |
| * the font to be used in the calculation |
| * @return the width of the ellipsis |
| * @since 3.4 |
| */ |
| private int getEllipsisWidth(Font font) { |
| return getTextUtilities().getTextExtents(TextFlow.ELLIPSIS, font).width; |
| } |
| } |