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
|
}
|