Project

General

Profile

Download (11.6 KB) Statistics
| Branch: | Tag: | Revision:
1
/*******************************************************************************
2
 * Copyright (c) 2000, 2010 IBM Corporation and others.
3
 * All rights reserved. This program and the accompanying materials
4
 * are made available under the terms of the Eclipse Public License v1.0
5
 * which accompanies this distribution, and is available at
6
 * http://www.eclipse.org/legal/epl-v10.html
7
 *
8
 * Contributors:
9
 *     IBM Corporation - initial API and implementation
10
 *******************************************************************************/
11
package org.eclipse.draw2d.text;
12

    
13
import com.ibm.icu.text.BreakIterator;
14

    
15
import org.eclipse.swt.graphics.Font;
16
import org.eclipse.swt.graphics.Rectangle;
17
import org.eclipse.swt.graphics.TextLayout;
18
import org.eclipse.swt.widgets.Display;
19

    
20
import org.eclipse.draw2d.FigureUtilities;
21
import org.eclipse.draw2d.TextUtilities;
22
import org.eclipse.draw2d.rap.swt.SWT;
23

    
24
/**
25
 * Utility class for FlowFigures.
26
 * 
27
 * @author hudsonr
28
 * @since 3.4
29
 */
30
public class FlowUtilities {
31

    
32
	interface LookAhead {
33
		int getWidth();
34
	}
35

    
36
	/**
37
	 * a singleton default instance
38
	 */
39
	public static FlowUtilities INSTANCE = new FlowUtilities();
40

    
41
	private static final BreakIterator INTERNAL_LINE_BREAK = BreakIterator
42
			.getLineInstance();
43
	private static TextLayout layout;
44

    
45
	static final BreakIterator LINE_BREAK = BreakIterator.getLineInstance();
46

    
47
	static boolean canBreakAfter(char c) {
48
		boolean result = Character.isWhitespace(c) || c == '-';
49
		if (!result && (c < 'a' || c > 'z')) {
50
			// chinese characters and such would be caught in here
51
			// LINE_BREAK is used here because INTERNAL_LINE_BREAK might be in
52
			// use
53
			LINE_BREAK.setText(c + "a"); //$NON-NLS-1$
54
			result = LINE_BREAK.isBoundary(1);
55
		}
56
		return result;
57
	}
58

    
59
	private static int findFirstDelimeter(String string) {
60
		int macNL = string.indexOf('\r');
61
		int unixNL = string.indexOf('\n');
62

    
63
		if (macNL == -1)
64
			macNL = Integer.MAX_VALUE;
65
		if (unixNL == -1)
66
			unixNL = Integer.MAX_VALUE;
67

    
68
		return Math.min(macNL, unixNL);
69
	}
70

    
71
	/**
72
	 * Gets the average character width.
73
	 * 
74
	 * @param fragment
75
	 *            the supplied TextFragmentBox to use for calculation. if the
76
	 *            length is 0 or if the width is or below 0, the average
77
	 *            character width is taken from standard font metrics.
78
	 * @param font
79
	 *            the font to use in case the TextFragmentBox conditions above
80
	 *            are true.
81
	 * @return the average character width
82
	 */
83
	protected float getAverageCharWidth(TextFragmentBox fragment, Font font) {
84
		if (fragment.getWidth() > 0 && fragment.length != 0)
85
			return fragment.getWidth() / (float) fragment.length;
86
		return FigureUtilities.getFontMetrics(font).getAverageCharWidth();
87
	}
88

    
89
	static int getBorderAscent(InlineFlow owner) {
90
		if (owner.getBorder() instanceof FlowBorder) {
91
			FlowBorder border = (FlowBorder) owner.getBorder();
92
			return border.getInsets(owner).top;
93
		}
94
		return 0;
95
	}
96

    
97
	static int getBorderAscentWithMargin(InlineFlow owner) {
98
		if (owner.getBorder() instanceof FlowBorder) {
99
			FlowBorder border = (FlowBorder) owner.getBorder();
100
			return border.getTopMargin() + border.getInsets(owner).top;
101
		}
102
		return 0;
103
	}
104

    
105
	static int getBorderDescent(InlineFlow owner) {
106
		if (owner.getBorder() instanceof FlowBorder) {
107
			FlowBorder border = (FlowBorder) owner.getBorder();
108
			return border.getInsets(owner).bottom;
109
		}
110
		return 0;
111
	}
112

    
113
	static int getBorderDescentWithMargin(InlineFlow owner) {
114
		if (owner.getBorder() instanceof FlowBorder) {
115
			FlowBorder border = (FlowBorder) owner.getBorder();
116
			return border.getBottomMargin() + border.getInsets(owner).bottom;
117
		}
118
		return 0;
119
	}
120

    
121
	/**
122
	 * Provides a TextLayout that can be used by the Draw2d text package for
123
	 * Bidi. This TextLayout should not be disposed by clients. The provided
124
	 * TextLayout's orientation will be LTR.
125
	 * 
126
	 * @return an SWT TextLayout that can be used for Bidi
127
	 * @since 3.1
128
	 */
129
	static TextLayout getTextLayout() {
130
		if (layout == null)
131
			layout = new TextLayout(Display.getDefault());
132
		layout.setOrientation(SWT.LEFT_TO_RIGHT);
133
		return layout;
134
	}
135

    
136
	/**
137
	 * @param frag
138
	 * @param string
139
	 * @param font
140
	 * @since 3.1
141
	 */
142
	private static void initBidi(TextFragmentBox frag, String string, Font font) {
143
		if (frag.requiresBidi()) {
144
			TextLayout textLayout = getTextLayout();
145
			textLayout.setFont(font);
146
			// $TODO need to insert overrides in front of string.
147
			textLayout.setText(string);
148
		}
149
	}
150

    
151
	private int measureString(TextFragmentBox frag, String string, int guess,
152
			Font font) {
153
		if (frag.requiresBidi()) {
154
			// The text and/or could have changed if the lookAhead was invoked.
155
			// This will
156
			// happen at most once.
157
			return getTextLayoutBounds(string, font, 0, guess - 1).width;
158
		} else
159
			return getTextUtilities().getTextExtents(
160
					string.substring(0, guess), font).width;
161
	}
162

    
163
	/**
164
	 * Sets up the fragment width based using the font and string passed in.
165
	 * 
166
	 * @param fragment
167
	 *            the text fragment whose width will be set
168
	 * @param font
169
	 *            the font to be used in the calculation
170
	 * @param string
171
	 *            the string to be used in the calculation
172
	 */
173
	final protected void setupFragment(TextFragmentBox fragment, Font font,
174
			String string) {
175
		if (fragment.getWidth() == -1 || fragment.isTruncated()) {
176
			int width;
177
			if (string.length() == 0 || fragment.length == 0)
178
				width = 0;
179
			else if (fragment.requiresBidi()) {
180
				width = getTextLayoutBounds(string, font, 0,
181
						fragment.length - 1).width;
182
			} else
183
				width = getTextUtilities().getTextExtents(
184
						string.substring(0, fragment.length), font).width;
185
			if (fragment.isTruncated())
186
				width += getEllipsisWidth(font);
187
			fragment.setWidth(width);
188
		}
189
	}
190

    
191
	/**
192
	 * Sets up a fragment and returns the number of characters consumed from the
193
	 * given String. An average character width can be provided as a hint for
194
	 * faster calculation. If a fragment's bidi level is set, a TextLayout will
195
	 * be used to calculate the width.
196
	 * 
197
	 * @param frag
198
	 *            the TextFragmentBox
199
	 * @param string
200
	 *            the String
201
	 * @param font
202
	 *            the Font used for measuring
203
	 * @param context
204
	 *            the flow context
205
	 * @param wrapping
206
	 *            the word wrap style
207
	 * @return the number of characters that will fit in the given space; can be
208
	 *         0 (eg., when the first character of the given string is a
209
	 *         newline)
210
	 */
211
	final protected int wrapFragmentInContext(TextFragmentBox frag,
212
			String string, FlowContext context, LookAhead lookahead, Font font,
213
			int wrapping) {
214
		frag.setTruncated(false);
215
		int strLen = string.length();
216
		if (strLen == 0) {
217
			frag.setWidth(-1);
218
			frag.length = 0;
219
			setupFragment(frag, font, string);
220
			context.addToCurrentLine(frag);
221
			return 0;
222
		}
223

    
224
		INTERNAL_LINE_BREAK.setText(string);
225

    
226
		initBidi(frag, string, font);
227
		float avgCharWidth = getAverageCharWidth(frag, font);
228
		frag.setWidth(-1);
229

    
230
		/*
231
		 * Setup initial boundaries within the string.
232
		 */
233
		int absoluteMin = 0;
234
		int max, min = 1;
235
		if (wrapping == ParagraphTextLayout.WORD_WRAP_HARD) {
236
			absoluteMin = INTERNAL_LINE_BREAK.next();
237
			while (absoluteMin > 0
238
					&& Character.isWhitespace(string.charAt(absoluteMin - 1)))
239
				absoluteMin--;
240
			min = Math.max(absoluteMin, 1);
241
		}
242
		int firstDelimiter = findFirstDelimeter(string);
243
		if (firstDelimiter == 0)
244
			min = max = 0;
245
		else
246
			max = Math.min(strLen, firstDelimiter) + 1;
247

    
248
		int availableWidth = context.getRemainingLineWidth();
249
		int guess = 0, guessSize = 0;
250

    
251
		while (true) {
252
			if ((max - min) <= 1) {
253
				if (min == absoluteMin
254
						&& context.isCurrentLineOccupied()
255
						&& !context.getContinueOnSameLine()
256
						&& availableWidth < measureString(frag, string, min,
257
								font)
258
								+ ((min == strLen && lookahead != null) ? lookahead
259
										.getWidth() : 0)) {
260
					context.endLine();
261
					availableWidth = context.getRemainingLineWidth();
262
					max = Math.min(strLen, firstDelimiter) + 1;
263
					if ((max - min) <= 1)
264
						break;
265
				} else
266
					break;
267
			}
268
			// Pick a new guess size
269
			// New guess is the last guess plus the missing width in pixels
270
			// divided by the average character size in pixels
271
			guess += 0.5f + (availableWidth - guessSize) / avgCharWidth;
272

    
273
			if (guess >= max)
274
				guess = max - 1;
275
			if (guess <= min)
276
				guess = min + 1;
277

    
278
			guessSize = measureString(frag, string, guess, font);
279

    
280
			if (guess == strLen && lookahead != null
281
					&& !canBreakAfter(string.charAt(strLen - 1))
282
					&& guessSize + lookahead.getWidth() > availableWidth) {
283
				max = guess;
284
				continue;
285
			}
286

    
287
			if (guessSize <= availableWidth) {
288
				min = guess;
289
				frag.setWidth(guessSize);
290
				if (guessSize == availableWidth)
291
					max = guess + 1;
292
			} else
293
				max = guess;
294
		}
295

    
296
		int result = min;
297
		boolean continueOnLine = false;
298
		if (min == strLen) {
299
			// Everything fits
300
			if (string.charAt(strLen - 1) == ' ') {
301
				if (frag.getWidth() == -1) {
302
					frag.length = result;
303
					frag.setWidth(measureString(frag, string, result, font));
304
				}
305
				if (lookahead.getWidth() > availableWidth - frag.getWidth()) {
306
					frag.length = result - 1;
307
					frag.setWidth(-1);
308
				} else
309
					frag.length = result;
310
			} else {
311
				continueOnLine = !canBreakAfter(string.charAt(strLen - 1));
312
				frag.length = result;
313
			}
314
		} else if (min == firstDelimiter) {
315
			// move result past the delimiter
316
			frag.length = result;
317
			if (string.charAt(min) == '\r') {
318
				result++;
319
				if (++min < strLen && string.charAt(min) == '\n')
320
					result++;
321
			} else if (string.charAt(min) == '\n')
322
				result++;
323
		} else if (string.charAt(min) == ' '
324
				|| canBreakAfter(string.charAt(min - 1))
325
				|| INTERNAL_LINE_BREAK.isBoundary(min)) {
326
			frag.length = min;
327
			if (string.charAt(min) == ' ')
328
				result++;
329
			else if (string.charAt(min - 1) == ' ') {
330
				frag.length--;
331
				frag.setWidth(-1);
332
			}
333
		} else
334
			out: {
335
				// In the middle of an unbreakable offset
336
				result = INTERNAL_LINE_BREAK.preceding(min);
337
				if (result == 0) {
338
					switch (wrapping) {
339
					case ParagraphTextLayout.WORD_WRAP_TRUNCATE:
340
						int truncatedWidth = availableWidth
341
								- getEllipsisWidth(font);
342
						if (truncatedWidth > 0) {
343
							// $TODO this is very slow. It should be using
344
							// avgCharWidth to go faster
345
							while (min > 0) {
346
								guessSize = measureString(frag, string, min,
347
										font);
348
								if (guessSize <= truncatedWidth)
349
									break;
350
								min--;
351
							}
352
							frag.length = min;
353
						} else
354
							frag.length = 0;
355
						frag.setTruncated(true);
356
						result = INTERNAL_LINE_BREAK.following(max - 1);
357
						break out;
358

    
359
					default:
360
						result = min;
361
						break;
362
					}
363
				}
364
				frag.length = result;
365
				if (string.charAt(result - 1) == ' ')
366
					frag.length--;
367
				frag.setWidth(-1);
368
			}
369

    
370
		setupFragment(frag, font, string);
371
		context.addToCurrentLine(frag);
372
		context.setContinueOnSameLine(continueOnLine);
373
		return result;
374
	}
375

    
376
	/**
377
	 * @see TextLayout#getBounds()
378
	 */
379
	protected Rectangle getTextLayoutBounds(String s, Font f, int start, int end) {
380
		TextLayout textLayout = getTextLayout();
381
		textLayout.setFont(f);
382
		textLayout.setText(s);
383
		return textLayout.getBounds(start, end);
384
	}
385

    
386
	/**
387
	 * Returns an instance of a <code>TextUtililities</code> class on which text
388
	 * calculations can be performed. Clients may override to customize.
389
	 * 
390
	 * @return the <code>TextUtililities</code> instance
391
	 * @since 3.4
392
	 */
393
	protected TextUtilities getTextUtilities() {
394
		return TextUtilities.INSTANCE;
395
	}
396

    
397
	/**
398
	 * Gets the ellipsis width.
399
	 * 
400
	 * @param font
401
	 *            the font to be used in the calculation
402
	 * @return the width of the ellipsis
403
	 * @since 3.4
404
	 */
405
	private int getEllipsisWidth(Font font) {
406
		return getTextUtilities().getTextExtents(TextFlow.ELLIPSIS, font).width;
407
	}
408
}
(19-19/31)