/******************************************************************************* * 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.rap.rwt.RWT; import org.eclipse.rap.rwt.SingletonUtil; 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; import org.eclipse.swt.SWT; /** * Utility class for FlowFigures. * * @author hudsonr * @since 3.4 */ public class FlowUtilities { interface LookAhead { int getWidth(); } /** * a singleton default instance */ public static FlowUtilities INSTANCE(){ return SingletonUtil.getSessionInstance(FlowUtilities.class); } private final BreakIterator INTERNAL_LINE_BREAK = BreakIterator .getLineInstance(RWT.getLocale()); private TextLayout layout; final BreakIterator LINE_BREAK = BreakIterator.getLineInstance(RWT.getLocale()); 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 */ 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 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; } }