1 /*
2 * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25 package javax.swing.text;
26
27 import java.util.Arrays;
28 import java.awt;
29 import java.awt.font.TextAttribute;
30 import javax.swing.event;
31 import javax.swing.SizeRequirements;
32
33 /**
34 * View of a simple line-wrapping paragraph that supports
35 * multiple fonts, colors, components, icons, etc. It is
36 * basically a vertical box with a margin around it. The
37 * contents of the box are a bunch of rows which are special
38 * horizontal boxes. This view creates a collection of
39 * views that represent the child elements of the paragraph
40 * element. Each of these views are placed into a row
41 * directly if they will fit, otherwise the <code>breakView</code>
42 * method is called to try and carve the view into pieces
43 * that fit.
44 *
45 * @author Timothy Prinzing
46 * @author Scott Violet
47 * @author Igor Kushnirskiy
48 * @see View
49 */
50 public class ParagraphView extends FlowView implements TabExpander {
51
52 /**
53 * Constructs a <code>ParagraphView</code> for the given element.
54 *
55 * @param elem the element that this view is responsible for
56 */
57 public ParagraphView(Element elem) {
58 super(elem, View.Y_AXIS);
59 setPropertiesFromAttributes();
60 Document doc = elem.getDocument();
61 Object i18nFlag = doc.getProperty(AbstractDocument.I18NProperty);
62 if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) {
63 try {
64 if (i18nStrategy == null) {
65 // the classname should probably come from a property file.
66 String classname = "javax.swing.text.TextLayoutStrategy";
67 ClassLoader loader = getClass().getClassLoader();
68 if (loader != null) {
69 i18nStrategy = loader.loadClass(classname);
70 } else {
71 i18nStrategy = Class.forName(classname);
72 }
73 }
74 Object o = i18nStrategy.newInstance();
75 if (o instanceof FlowStrategy) {
76 strategy = (FlowStrategy) o;
77 }
78 } catch (Throwable e) {
79 throw new StateInvariantError("ParagraphView: Can't create i18n strategy: "
80 + e.getMessage());
81 }
82 }
83 }
84
85 /**
86 * Sets the type of justification.
87 *
88 * @param j one of the following values:
89 * <ul>
90 * <li><code>StyleConstants.ALIGN_LEFT</code>
91 * <li><code>StyleConstants.ALIGN_CENTER</code>
92 * <li><code>StyleConstants.ALIGN_RIGHT</code>
93 * </ul>
94 */
95 protected void setJustification(int j) {
96 justification = j;
97 }
98
99 /**
100 * Sets the line spacing.
101 *
102 * @param ls the value is a factor of the line hight
103 */
104 protected void setLineSpacing(float ls) {
105 lineSpacing = ls;
106 }
107
108 /**
109 * Sets the indent on the first line.
110 *
111 * @param fi the value in points
112 */
113 protected void setFirstLineIndent(float fi) {
114 firstLineIndent = (int) fi;
115 }
116
117 /**
118 * Set the cached properties from the attributes.
119 */
120 protected void setPropertiesFromAttributes() {
121 AttributeSet attr = getAttributes();
122 if (attr != null) {
123 setParagraphInsets(attr);
124 Integer a = (Integer)attr.getAttribute(StyleConstants.Alignment);
125 int alignment;
126 if (a == null) {
127 Document doc = getElement().getDocument();
128 Object o = doc.getProperty(TextAttribute.RUN_DIRECTION);
129 if ((o != null) && o.equals(TextAttribute.RUN_DIRECTION_RTL)) {
130 alignment = StyleConstants.ALIGN_RIGHT;
131 } else {
132 alignment = StyleConstants.ALIGN_LEFT;
133 }
134 } else {
135 alignment = a.intValue();
136 }
137 setJustification(alignment);
138 setLineSpacing(StyleConstants.getLineSpacing(attr));
139 setFirstLineIndent(StyleConstants.getFirstLineIndent(attr));
140 }
141 }
142
143 /**
144 * Returns the number of views that this view is
145 * responsible for.
146 * The child views of the paragraph are rows which
147 * have been used to arrange pieces of the <code>View</code>s
148 * that represent the child elements. This is the number
149 * of views that have been tiled in two dimensions,
150 * and should be equivalent to the number of child elements
151 * to the element this view is responsible for.
152 *
153 * @return the number of views that this <code>ParagraphView</code>
154 * is responsible for
155 */
156 protected int getLayoutViewCount() {
157 return layoutPool.getViewCount();
158 }
159
160 /**
161 * Returns the view at a given <code>index</code>.
162 * The child views of the paragraph are rows which
163 * have been used to arrange pieces of the <code>Views</code>
164 * that represent the child elements. This methods returns
165 * the view responsible for the child element index
166 * (prior to breaking). These are the Views that were
167 * produced from a factory (to represent the child
168 * elements) and used for layout.
169 *
170 * @param index the <code>index</code> of the desired view
171 * @return the view at <code>index</code>
172 */
173 protected View getLayoutView(int index) {
174 return layoutPool.getView(index);
175 }
176
177 /**
178 * Adjusts the given row if possible to fit within the
179 * layout span. By default this will try to find the
180 * highest break weight possible nearest the end of
181 * the row. If a forced break is encountered, the
182 * break will be positioned there.
183 * <p>
184 * This is meant for internal usage, and should not be used directly.
185 *
186 * @param r the row to adjust to the current layout
187 * span
188 * @param desiredSpan the current layout span >= 0
189 * @param x the location r starts at
190 */
191 protected void adjustRow(Row r, int desiredSpan, int x) {
192 }
193
194 /**
195 * Returns the next visual position for the cursor, in
196 * either the east or west direction.
197 * Overridden from <code>CompositeView</code>.
198 * @param pos position into the model
199 * @param b either <code>Position.Bias.Forward</code> or
200 * <code>Position.Bias.Backward</code>
201 * @param a the allocated region to render into
202 * @param direction either <code>SwingConstants.NORTH</code>
203 * or <code>SwingConstants.SOUTH</code>
204 * @param biasRet an array containing the bias that were checked
205 * in this method
206 * @return the location in the model that represents the
207 * next location visual position
208 */
209 protected int getNextNorthSouthVisualPositionFrom(int pos, Position.Bias b,
210 Shape a, int direction,
211 Position.Bias[] biasRet)
212 throws BadLocationException {
213 int vIndex;
214 if(pos == -1) {
215 vIndex = (direction == NORTH) ?
216 getViewCount() - 1 : 0;
217 }
218 else {
219 if(b == Position.Bias.Backward && pos > 0) {
220 vIndex = getViewIndexAtPosition(pos - 1);
221 }
222 else {
223 vIndex = getViewIndexAtPosition(pos);
224 }
225 if(direction == NORTH) {
226 if(vIndex == 0) {
227 return -1;
228 }
229 vIndex--;
230 }
231 else if(++vIndex >= getViewCount()) {
232 return -1;
233 }
234 }
235 // vIndex gives index of row to look in.
236 JTextComponent text = (JTextComponent)getContainer();
237 Caret c = text.getCaret();
238 Point magicPoint;
239 magicPoint = (c != null) ? c.getMagicCaretPosition() : null;
240 int x;
241 if(magicPoint == null) {
242 Shape posBounds;
243 try {
244 posBounds = text.getUI().modelToView(text, pos, b);
245 } catch (BadLocationException exc) {
246 posBounds = null;
247 }
248 if(posBounds == null) {
249 x = 0;
250 }
251 else {
252 x = posBounds.getBounds().x;
253 }
254 }
255 else {
256 x = magicPoint.x;
257 }
258 return getClosestPositionTo(pos, b, a, direction, biasRet, vIndex, x);
259 }
260
261 /**
262 * Returns the closest model position to <code>x</code>.
263 * <code>rowIndex</code> gives the index of the view that corresponds
264 * that should be looked in.
265 * @param pos position into the model
266 * @param a the allocated region to render into
267 * @param direction one of the following values:
268 * <ul>
269 * <li><code>SwingConstants.NORTH</code>
270 * <li><code>SwingConstants.SOUTH</code>
271 * </ul>
272 * @param biasRet an array containing the bias that were checked
273 * in this method
274 * @param rowIndex the index of the view
275 * @param x the x coordinate of interest
276 * @return the closest model position to <code>x</code>
277 */
278 // NOTE: This will not properly work if ParagraphView contains
279 // other ParagraphViews. It won't raise, but this does not message
280 // the children views with getNextVisualPositionFrom.
281 protected int getClosestPositionTo(int pos, Position.Bias b, Shape a,
282 int direction, Position.Bias[] biasRet,
283 int rowIndex, int x)
284 throws BadLocationException {
285 JTextComponent text = (JTextComponent)getContainer();
286 Document doc = getDocument();
287 AbstractDocument aDoc = (doc instanceof AbstractDocument) ?
288 (AbstractDocument)doc : null;
289 View row = getView(rowIndex);
290 int lastPos = -1;
291 // This could be made better to check backward positions too.
292 biasRet[0] = Position.Bias.Forward;
293 for(int vc = 0, numViews = row.getViewCount(); vc < numViews; vc++) {
294 View v = row.getView(vc);
295 int start = v.getStartOffset();
296 boolean ltr = (aDoc != null) ? aDoc.isLeftToRight
297 (start, start + 1) : true;
298 if(ltr) {
299 lastPos = start;
300 for(int end = v.getEndOffset(); lastPos < end; lastPos++) {
301 float xx = text.modelToView(lastPos).getBounds().x;
302 if(xx >= x) {
303 while (++lastPos < end &&
304 text.modelToView(lastPos).getBounds().x == xx) {
305 }
306 return --lastPos;
307 }
308 }
309 lastPos--;
310 }
311 else {
312 for(lastPos = v.getEndOffset() - 1; lastPos >= start;
313 lastPos--) {
314 float xx = text.modelToView(lastPos).getBounds().x;
315 if(xx >= x) {
316 while (--lastPos >= start &&
317 text.modelToView(lastPos).getBounds().x == xx) {
318 }
319 return ++lastPos;
320 }
321 }
322 lastPos++;
323 }
324 }
325 if(lastPos == -1) {
326 return getStartOffset();
327 }
328 return lastPos;
329 }
330
331 /**
332 * Determines in which direction the next view lays.
333 * Consider the <code>View</code> at index n.
334 * Typically the <code>View</code>s are layed out
335 * from left to right, so that the <code>View</code>
336 * to the EAST will be at index n + 1, and the
337 * <code>View</code> to the WEST will be at index n - 1.
338 * In certain situations, such as with bidirectional text,
339 * it is possible that the <code>View</code> to EAST is not
340 * at index n + 1, but rather at index n - 1,
341 * or that the <code>View</code> to the WEST is not at
342 * index n - 1, but index n + 1. In this case this method
343 * would return true, indicating the <code>View</code>s are
344 * layed out in descending order.
345 * <p>
346 * This will return true if the text is layed out right
347 * to left at position, otherwise false.
348 *
349 * @param position position into the model
350 * @param bias either <code>Position.Bias.Forward</code> or
351 * <code>Position.Bias.Backward</code>
352 * @return true if the text is layed out right to left at
353 * position, otherwise false.
354 */
355 protected boolean flipEastAndWestAtEnds(int position,
356 Position.Bias bias) {
357 Document doc = getDocument();
358 if(doc instanceof AbstractDocument &&
359 !((AbstractDocument)doc).isLeftToRight(getStartOffset(),
360 getStartOffset() + 1)) {
361 return true;
362 }
363 return false;
364 }
365
366 // --- FlowView methods ---------------------------------------------
367
368 /**
369 * Fetches the constraining span to flow against for
370 * the given child index.
371 * @param index the index of the view being queried
372 * @return the constraining span for the given view at
373 * <code>index</code>
374 * @since 1.3
375 */
376 public int getFlowSpan(int index) {
377 View child = getView(index);
378 int adjust = 0;
379 if (child instanceof Row) {
380 Row row = (Row) child;
381 adjust = row.getLeftInset() + row.getRightInset();
382 }
383 return (layoutSpan == Integer.MAX_VALUE) ? layoutSpan
384 : (layoutSpan - adjust);
385 }
386
387 /**
388 * Fetches the location along the flow axis that the
389 * flow span will start at.
390 * @param index the index of the view being queried
391 * @return the location for the given view at
392 * <code>index</code>
393 * @since 1.3
394 */
395 public int getFlowStart(int index) {
396 View child = getView(index);
397 int adjust = 0;
398 if (child instanceof Row) {
399 Row row = (Row) child;
400 adjust = row.getLeftInset();
401 }
402 return tabBase + adjust;
403 }
404
405 /**
406 * Create a <code>View</code> that should be used to hold a
407 * a row's worth of children in a flow.
408 * @return the new <code>View</code>
409 * @since 1.3
410 */
411 protected View createRow() {
412 return new Row(getElement());
413 }
414
415 // --- TabExpander methods ------------------------------------------
416
417 /**
418 * Returns the next tab stop position given a reference position.
419 * This view implements the tab coordinate system, and calls
420 * <code>getTabbedSpan</code> on the logical children in the process
421 * of layout to determine the desired span of the children. The
422 * logical children can delegate their tab expansion upward to
423 * the paragraph which knows how to expand tabs.
424 * <code>LabelView</code> is an example of a view that delegates
425 * its tab expansion needs upward to the paragraph.
426 * <p>
427 * This is implemented to try and locate a <code>TabSet</code>
428 * in the paragraph element's attribute set. If one can be
429 * found, its settings will be used, otherwise a default expansion
430 * will be provided. The base location for for tab expansion
431 * is the left inset from the paragraphs most recent allocation
432 * (which is what the layout of the children is based upon).
433 *
434 * @param x the X reference position
435 * @param tabOffset the position within the text stream
436 * that the tab occurred at >= 0
437 * @return the trailing end of the tab expansion >= 0
438 * @see TabSet
439 * @see TabStop
440 * @see LabelView
441 */
442 public float nextTabStop(float x, int tabOffset) {
443 // If the text isn't left justified, offset by 10 pixels!
444 if(justification != StyleConstants.ALIGN_LEFT)
445 return x + 10.0f;
446 x -= tabBase;
447 TabSet tabs = getTabSet();
448 if(tabs == null) {
449 // a tab every 72 pixels.
450 return (float)(tabBase + (((int)x / 72 + 1) * 72));
451 }
452 TabStop tab = tabs.getTabAfter(x + .01f);
453 if(tab == null) {
454 // no tab, do a default of 5 pixels.
455 // Should this cause a wrapping of the line?
456 return tabBase + x + 5.0f;
457 }
458 int alignment = tab.getAlignment();
459 int offset;
460 switch(alignment) {
461 default:
462 case TabStop.ALIGN_LEFT:
463 // Simple case, left tab.
464 return tabBase + tab.getPosition();
465 case TabStop.ALIGN_BAR:
466 // PENDING: what does this mean?
467 return tabBase + tab.getPosition();
468 case TabStop.ALIGN_RIGHT:
469 case TabStop.ALIGN_CENTER:
470 offset = findOffsetToCharactersInString(tabChars,
471 tabOffset + 1);
472 break;
473 case TabStop.ALIGN_DECIMAL:
474 offset = findOffsetToCharactersInString(tabDecimalChars,
475 tabOffset + 1);
476 break;
477 }
478 if (offset == -1) {
479 offset = getEndOffset();
480 }
481 float charsSize = getPartialSize(tabOffset + 1, offset);
482 switch(alignment) {
483 case TabStop.ALIGN_RIGHT:
484 case TabStop.ALIGN_DECIMAL:
485 // right and decimal are treated the same way, the new
486 // position will be the location of the tab less the
487 // partialSize.
488 return tabBase + Math.max(x, tab.getPosition() - charsSize);
489 case TabStop.ALIGN_CENTER:
490 // Similar to right, but half the partialSize.
491 return tabBase + Math.max(x, tab.getPosition() - charsSize / 2.0f);
492 }
493 // will never get here!
494 return x;
495 }
496
497 /**
498 * Gets the <code>Tabset</code> to be used in calculating tabs.
499 *
500 * @return the <code>TabSet</code>
501 */
502 protected TabSet getTabSet() {
503 return StyleConstants.getTabSet(getElement().getAttributes());
504 }
505
506 /**
507 * Returns the size used by the views between
508 * <code>startOffset</code> and <code>endOffset</code>.
509 * This uses <code>getPartialView</code> to calculate the
510 * size if the child view implements the
511 * <code>TabableView</code> interface. If a
512 * size is needed and a <code>View</code> does not implement
513 * the <code>TabableView</code> interface,
514 * the <code>preferredSpan</code> will be used.
515 *
516 * @param startOffset the starting document offset >= 0
517 * @param endOffset the ending document offset >= startOffset
518 * @return the size >= 0
519 */
520 protected float getPartialSize(int startOffset, int endOffset) {
521 float size = 0.0f;
522 int viewIndex;
523 int numViews = getViewCount();
524 View view;
525 int viewEnd;
526 int tempEnd;
527
528 // Have to search layoutPool!
529 // PENDING: when ParagraphView supports breaking location
530 // into layoutPool will have to change!
531 viewIndex = getElement().getElementIndex(startOffset);
532 numViews = layoutPool.getViewCount();
533 while(startOffset < endOffset && viewIndex < numViews) {
534 view = layoutPool.getView(viewIndex++);
535 viewEnd = view.getEndOffset();
536 tempEnd = Math.min(endOffset, viewEnd);
537 if(view instanceof TabableView)
538 size += ((TabableView)view).getPartialSpan(startOffset, tempEnd);
539 else if(startOffset == view.getStartOffset() &&
540 tempEnd == view.getEndOffset())
541 size += view.getPreferredSpan(View.X_AXIS);
542 else
543 // PENDING: should we handle this better?
544 return 0.0f;
545 startOffset = viewEnd;
546 }
547 return size;
548 }
549
550 /**
551 * Finds the next character in the document with a character in
552 * <code>string</code>, starting at offset <code>start</code>. If
553 * there are no characters found, -1 will be returned.
554 *
555 * @param string the string of characters
556 * @param start where to start in the model >= 0
557 * @return the document offset, or -1 if no characters found
558 */
559 protected int findOffsetToCharactersInString(char[] string,
560 int start) {
561 int stringLength = string.length;
562 int end = getEndOffset();
563 Segment seg = new Segment();
564 try {
565 getDocument().getText(start, end - start, seg);
566 } catch (BadLocationException ble) {
567 return -1;
568 }
569 for(int counter = seg.offset, maxCounter = seg.offset + seg.count;
570 counter < maxCounter; counter++) {
571 char currentChar = seg.array[counter];
572 for(int subCounter = 0; subCounter < stringLength;
573 subCounter++) {
574 if(currentChar == string[subCounter])
575 return counter - seg.offset + start;
576 }
577 }
578 // No match.
579 return -1;
580 }
581
582 /**
583 * Returns where the tabs are calculated from.
584 * @return where tabs are calculated from
585 */
586 protected float getTabBase() {
587 return (float)tabBase;
588 }
589
590 // ---- View methods ----------------------------------------------------
591
592 /**
593 * Renders using the given rendering surface and area on that
594 * surface. This is implemented to delgate to the superclass
595 * after stashing the base coordinate for tab calculations.
596 *
597 * @param g the rendering surface to use
598 * @param a the allocated region to render into
599 * @see View#paint
600 */
601 public void paint(Graphics g, Shape a) {
602 Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
603 tabBase = alloc.x + getLeftInset();
604 super.paint(g, a);
605
606 // line with the negative firstLineIndent value needs
607 // special handling
608 if (firstLineIndent < 0) {
609 Shape sh = getChildAllocation(0, a);
610 if ((sh != null) && sh.intersects(alloc)) {
611 int x = alloc.x + getLeftInset() + firstLineIndent;
612 int y = alloc.y + getTopInset();
613
614 Rectangle clip = g.getClipBounds();
615 tempRect.x = x + getOffset(X_AXIS, 0);
616 tempRect.y = y + getOffset(Y_AXIS, 0);
617 tempRect.width = getSpan(X_AXIS, 0) - firstLineIndent;
618 tempRect.height = getSpan(Y_AXIS, 0);
619 if (tempRect.intersects(clip)) {
620 tempRect.x = tempRect.x - firstLineIndent;
621 paintChild(g, tempRect, 0);
622 }
623 }
624 }
625 }
626
627 /**
628 * Determines the desired alignment for this view along an
629 * axis. This is implemented to give the alignment to the
630 * center of the first row along the y axis, and the default
631 * along the x axis.
632 *
633 * @param axis may be either <code>View.X_AXIS</code> or
634 * <code>View.Y_AXIS</code>
635 * @return the desired alignment. This should be a value
636 * between 0.0 and 1.0 inclusive, where 0 indicates alignment at the
637 * origin and 1.0 indicates alignment to the full span
638 * away from the origin. An alignment of 0.5 would be the
639 * center of the view.
640 */
641 public float getAlignment(int axis) {
642 switch (axis) {
643 case Y_AXIS:
644 float a = 0.5f;
645 if (getViewCount() != 0) {
646 int paragraphSpan = (int) getPreferredSpan(View.Y_AXIS);
647 View v = getView(0);
648 int rowSpan = (int) v.getPreferredSpan(View.Y_AXIS);
649 a = (paragraphSpan != 0) ? ((float)(rowSpan / 2)) / paragraphSpan : 0;
650 }
651 return a;
652 case X_AXIS:
653 return 0.5f;
654 default:
655 throw new IllegalArgumentException("Invalid axis: " + axis);
656 }
657 }
658
659 /**
660 * Breaks this view on the given axis at the given length.
661 * <p>
662 * <code>ParagraphView</code> instances are breakable
663 * along the <code>Y_AXIS</code> only, and only if
664 * <code>len</code> is after the first line.
665 *
666 * @param axis may be either <code>View.X_AXIS</code>
667 * or <code>View.Y_AXIS</code>
668 * @param len specifies where a potential break is desired
669 * along the given axis >= 0
670 * @param a the current allocation of the view
671 * @return the fragment of the view that represents the
672 * given span, if the view can be broken; if the view
673 * doesn't support breaking behavior, the view itself is
674 * returned
675 * @see View#breakView
676 */
677 public View breakView(int axis, float len, Shape a) {
678 if(axis == View.Y_AXIS) {
679 if(a != null) {
680 Rectangle alloc = a.getBounds();
681 setSize(alloc.width, alloc.height);
682 }
683 // Determine what row to break on.
684
685 // PENDING(prinz) add break support
686 return this;
687 }
688 return this;
689 }
690
691 /**
692 * Gets the break weight for a given location.
693 * <p>
694 * <code>ParagraphView</code> instances are breakable
695 * along the <code>Y_AXIS</code> only, and only if
696 * <code>len</code> is after the first row. If the length
697 * is less than one row, a value of <code>BadBreakWeight</code>
698 * is returned.
699 *
700 * @param axis may be either <code>View.X_AXIS</code>
701 * or <code>View.Y_AXIS</code>
702 * @param len specifies where a potential break is desired >= 0
703 * @return a value indicating the attractiveness of breaking here;
704 * either <code>GoodBreakWeight</code> or <code>BadBreakWeight</code>
705 * @see View#getBreakWeight
706 */
707 public int getBreakWeight(int axis, float len) {
708 if(axis == View.Y_AXIS) {
709 // PENDING(prinz) make this return a reasonable value
710 // when paragraph breaking support is re-implemented.
711 // If less than one row, bad weight value should be
712 // returned.
713 //return GoodBreakWeight;
714 return BadBreakWeight;
715 }
716 return BadBreakWeight;
717 }
718
719 /**
720 * Calculate the needs for the paragraph along the minor axis.
721 *
722 * <p>This uses size requirements of the superclass, modified to take into
723 * account the non-breakable areas at the adjacent views edges. The minimal
724 * size requirements for such views should be no less than the sum of all
725 * adjacent fragments.</p>
726 *
727 * <p>If the {@code axis} parameter is neither {@code View.X_AXIS} nor
728 * {@code View.Y_AXIS}, {@link IllegalArgumentException} is thrown. If the
729 * {@code r} parameter is {@code null,} a new {@code SizeRequirements}
730 * object is created, otherwise the supplied {@code SizeRequirements}
731 * object is returned.</p>
732 *
733 * @param axis the minor axis
734 * @param r the input {@code SizeRequirements} object
735 * @return the new or adjusted {@code SizeRequirements} object
736 * @throw IllegalArgumentException if the {@code axis} parameter is invalid
737 */
738 @Override
739 protected SizeRequirements calculateMinorAxisRequirements(int axis,
740 SizeRequirements r) {
741 r = super.calculateMinorAxisRequirements(axis, r);
742
743 float min = 0;
744 float glue = 0;
745 int n = getLayoutViewCount();
746 for (int i = 0; i < n; i++) {
747 View v = getLayoutView(i);
748 float span = v.getMinimumSpan(axis);
749 if (v.getBreakWeight(axis, 0, v.getMaximumSpan(axis))
750 > View.BadBreakWeight) {
751 // find the longest non-breakable fragments at the view edges
752 int p0 = v.getStartOffset();
753 int p1 = v.getEndOffset();
754 float start = findEdgeSpan(v, axis, p0, p0, p1);
755 float end = findEdgeSpan(v, axis, p1, p0, p1);
756 glue += start;
757 min = Math.max(min, Math.max(span, glue));
758 glue = end;
759 } else {
760 // non-breakable view
761 glue += span;
762 min = Math.max(min, glue);
763 }
764 }
765 r.minimum = Math.max(r.minimum, (int) min);
766 r.preferred = Math.max(r.minimum, r.preferred);
767 r.maximum = Math.max(r.preferred, r.maximum);
768
769 return r;
770 }
771
772 /**
773 * Binary search for the longest non-breakable fragment at the view edge.
774 */
775 private float findEdgeSpan(View v, int axis, int fp, int p0, int p1) {
776 int len = p1 - p0;
777 if (len <= 1) {
778 // further fragmentation is not possible
779 return v.getMinimumSpan(axis);
780 } else {
781 int mid = p0 + len / 2;
782 boolean startEdge = mid > fp;
783 // initial view is breakable hence must support fragmentation
784 View f = startEdge ?
785 v.createFragment(fp, mid) : v.createFragment(mid, fp);
786 boolean breakable = f.getBreakWeight(
787 axis, 0, f.getMaximumSpan(axis)) > View.BadBreakWeight;
788 if (breakable == startEdge) {
789 p1 = mid;
790 } else {
791 p0 = mid;
792 }
793 return findEdgeSpan(f, axis, fp, p0, p1);
794 }
795 }
796
797 /**
798 * Gives notification from the document that attributes were changed
799 * in a location that this view is responsible for.
800 *
801 * @param changes the change information from the
802 * associated document
803 * @param a the current allocation of the view
804 * @param f the factory to use to rebuild if the view has children
805 * @see View#changedUpdate
806 */
807 public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
808 // update any property settings stored, and layout should be
809 // recomputed
810 setPropertiesFromAttributes();
811 layoutChanged(X_AXIS);
812 layoutChanged(Y_AXIS);
813 super.changedUpdate(changes, a, f);
814 }
815
816
817 // --- variables -----------------------------------------------
818
819 private int justification;
820 private float lineSpacing;
821 /** Indentation for the first line, from the left inset. */
822 protected int firstLineIndent = 0;
823
824 /**
825 * Used by the TabExpander functionality to determine
826 * where to base the tab calculations. This is basically
827 * the location of the left side of the paragraph.
828 */
829 private int tabBase;
830
831 /**
832 * Used to create an i18n-based layout strategy
833 */
834 static Class i18nStrategy;
835
836 /** Used for searching for a tab. */
837 static char[] tabChars;
838 /** Used for searching for a tab or decimal character. */
839 static char[] tabDecimalChars;
840
841 static {
842 tabChars = new char[1];
843 tabChars[0] = '\t';
844 tabDecimalChars = new char[2];
845 tabDecimalChars[0] = '\t';
846 tabDecimalChars[1] = '.';
847 }
848
849 /**
850 * Internally created view that has the purpose of holding
851 * the views that represent the children of the paragraph
852 * that have been arranged in rows.
853 */
854 class Row extends BoxView {
855
856 Row(Element elem) {
857 super(elem, View.X_AXIS);
858 }
859
860 /**
861 * This is reimplemented to do nothing since the
862 * paragraph fills in the row with its needed
863 * children.
864 */
865 protected void loadChildren(ViewFactory f) {
866 }
867
868 /**
869 * Fetches the attributes to use when rendering. This view
870 * isn't directly responsible for an element so it returns
871 * the outer classes attributes.
872 */
873 public AttributeSet getAttributes() {
874 View p = getParent();
875 return (p != null) ? p.getAttributes() : null;
876 }
877
878 public float getAlignment(int axis) {
879 if (axis == View.X_AXIS) {
880 switch (justification) {
881 case StyleConstants.ALIGN_LEFT:
882 return 0;
883 case StyleConstants.ALIGN_RIGHT:
884 return 1;
885 case StyleConstants.ALIGN_CENTER:
886 return 0.5f;
887 case StyleConstants.ALIGN_JUSTIFIED:
888 float rv = 0.5f;
889 //if we can justifiy the content always align to
890 //the left.
891 if (isJustifiableDocument()) {
892 rv = 0f;
893 }
894 return rv;
895 }
896 }
897 return super.getAlignment(axis);
898 }
899
900 /**
901 * Provides a mapping from the document model coordinate space
902 * to the coordinate space of the view mapped to it. This is
903 * implemented to let the superclass find the position along
904 * the major axis and the allocation of the row is used
905 * along the minor axis, so that even though the children
906 * are different heights they all get the same caret height.
907 *
908 * @param pos the position to convert
909 * @param a the allocated region to render into
910 * @return the bounding box of the given position
911 * @exception BadLocationException if the given position does not represent a
912 * valid location in the associated document
913 * @see View#modelToView
914 */
915 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
916 Rectangle r = a.getBounds();
917 View v = getViewAtPosition(pos, r);
918 if ((v != null) && (!v.getElement().isLeaf())) {
919 // Don't adjust the height if the view represents a branch.
920 return super.modelToView(pos, a, b);
921 }
922 r = a.getBounds();
923 int height = r.height;
924 int y = r.y;
925 Shape loc = super.modelToView(pos, a, b);
926 r = loc.getBounds();
927 r.height = height;
928 r.y = y;
929 return r;
930 }
931
932 /**
933 * Range represented by a row in the paragraph is only
934 * a subset of the total range of the paragraph element.
935 * @see View#getRange
936 */
937 public int getStartOffset() {
938 int offs = Integer.MAX_VALUE;
939 int n = getViewCount();
940 for (int i = 0; i < n; i++) {
941 View v = getView(i);
942 offs = Math.min(offs, v.getStartOffset());
943 }
944 return offs;
945 }
946
947 public int getEndOffset() {
948 int offs = 0;
949 int n = getViewCount();
950 for (int i = 0; i < n; i++) {
951 View v = getView(i);
952 offs = Math.max(offs, v.getEndOffset());
953 }
954 return offs;
955 }
956
957 /**
958 * Perform layout for the minor axis of the box (i.e. the
959 * axis orthoginal to the axis that it represents). The results
960 * of the layout should be placed in the given arrays which represent
961 * the allocations to the children along the minor axis.
962 * <p>
963 * This is implemented to do a baseline layout of the children
964 * by calling BoxView.baselineLayout.
965 *
966 * @param targetSpan the total span given to the view, which
967 * whould be used to layout the children.
968 * @param axis the axis being layed out.
969 * @param offsets the offsets from the origin of the view for
970 * each of the child views. This is a return value and is
971 * filled in by the implementation of this method.
972 * @param spans the span of each child view. This is a return
973 * value and is filled in by the implementation of this method.
974 * @return the offset and span for each child view in the
975 * offsets and spans parameters
976 */
977 protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
978 baselineLayout(targetSpan, axis, offsets, spans);
979 }
980
981 protected SizeRequirements calculateMinorAxisRequirements(int axis,
982 SizeRequirements r) {
983 return baselineRequirements(axis, r);
984 }
985
986
987 private boolean isLastRow() {
988 View parent;
989 return ((parent = getParent()) == null
990 || this == parent.getView(parent.getViewCount() - 1));
991 }
992
993 private boolean isBrokenRow() {
994 boolean rv = false;
995 int viewsCount = getViewCount();
996 if (viewsCount > 0) {
997 View lastView = getView(viewsCount - 1);
998 if (lastView.getBreakWeight(X_AXIS, 0, 0) >=
999 ForcedBreakWeight) {
1000 rv = true;
1001 }
1002 }
1003 return rv;
1004 }
1005
1006 private boolean isJustifiableDocument() {
1007 return (! Boolean.TRUE.equals(getDocument().getProperty(
1008 AbstractDocument.I18NProperty)));
1009 }
1010
1011 /**
1012 * Whether we need to justify this {@code Row}.
1013 * At this time (jdk1.6) we support justification on for non
1014 * 18n text.
1015 *
1016 * @return {@code true} if this {@code Row} should be justified.
1017 */
1018 private boolean isJustifyEnabled() {
1019 boolean ret = (justification == StyleConstants.ALIGN_JUSTIFIED);
1020
1021 //no justification for i18n documents
1022 ret = ret && isJustifiableDocument();
1023
1024 //no justification for the last row
1025 ret = ret && ! isLastRow();
1026
1027 //no justification for the broken rows
1028 ret = ret && ! isBrokenRow();
1029
1030 return ret;
1031 }
1032
1033
1034 //Calls super method after setting spaceAddon to 0.
1035 //Justification should not affect MajorAxisRequirements
1036 @Override
1037 protected SizeRequirements calculateMajorAxisRequirements(int axis,
1038 SizeRequirements r) {
1039 int oldJustficationData[] = justificationData;
1040 justificationData = null;
1041 SizeRequirements ret = super.calculateMajorAxisRequirements(axis, r);
1042 if (isJustifyEnabled()) {
1043 justificationData = oldJustficationData;
1044 }
1045 return ret;
1046 }
1047
1048 @Override
1049 protected void layoutMajorAxis(int targetSpan, int axis,
1050 int[] offsets, int[] spans) {
1051 int oldJustficationData[] = justificationData;
1052 justificationData = null;
1053 super.layoutMajorAxis(targetSpan, axis, offsets, spans);
1054 if (! isJustifyEnabled()) {
1055 return;
1056 }
1057
1058 int currentSpan = 0;
1059 for (int span : spans) {
1060 currentSpan += span;
1061 }
1062 if (currentSpan == targetSpan) {
1063 //no need to justify
1064 return;
1065 }
1066
1067 // we justify text by enlarging spaces by the {@code spaceAddon}.
1068 // justification is started to the right of the rightmost TAB.
1069 // leading and trailing spaces are not extendable.
1070 //
1071 // GlyphPainter1 uses
1072 // justificationData
1073 // for all painting and measurement.
1074
1075 int extendableSpaces = 0;
1076 int startJustifiableContent = -1;
1077 int endJustifiableContent = -1;
1078 int lastLeadingSpaces = 0;
1079
1080 int rowStartOffset = getStartOffset();
1081 int rowEndOffset = getEndOffset();
1082 int spaceMap[] = new int[rowEndOffset - rowStartOffset];
1083 Arrays.fill(spaceMap, 0);
1084 for (int i = getViewCount() - 1; i >= 0 ; i--) {
1085 View view = getView(i);
1086 if (view instanceof GlyphView) {
1087 GlyphView.JustificationInfo justificationInfo =
1088 ((GlyphView) view).getJustificationInfo(rowStartOffset);
1089 final int viewStartOffset = view.getStartOffset();
1090 final int offset = viewStartOffset - rowStartOffset;
1091 for (int j = 0; j < justificationInfo.spaceMap.length(); j++) {
1092 if (justificationInfo.spaceMap.get(j)) {
1093 spaceMap[j + offset] = 1;
1094 }
1095 }
1096 if (startJustifiableContent > 0) {
1097 if (justificationInfo.end >= 0) {
1098 extendableSpaces += justificationInfo.trailingSpaces;
1099 } else {
1100 lastLeadingSpaces += justificationInfo.trailingSpaces;
1101 }
1102 }
1103 if (justificationInfo.start >= 0) {
1104 startJustifiableContent =
1105 justificationInfo.start + viewStartOffset;
1106 extendableSpaces += lastLeadingSpaces;
1107 }
1108 if (justificationInfo.end >= 0
1109 && endJustifiableContent < 0) {
1110 endJustifiableContent =
1111 justificationInfo.end + viewStartOffset;
1112 }
1113 extendableSpaces += justificationInfo.contentSpaces;
1114 lastLeadingSpaces = justificationInfo.leadingSpaces;
1115 if (justificationInfo.hasTab) {
1116 break;
1117 }
1118 }
1119 }
1120 if (extendableSpaces <= 0) {
1121 //there is nothing we can do to justify
1122 return;
1123 }
1124 int adjustment = (targetSpan - currentSpan);
1125 int spaceAddon = (extendableSpaces > 0)
1126 ? adjustment / extendableSpaces
1127 : 0;
1128 int spaceAddonLeftoverEnd = -1;
1129 for (int i = startJustifiableContent - rowStartOffset,
1130 leftover = adjustment - spaceAddon * extendableSpaces;
1131 leftover > 0;
1132 leftover -= spaceMap[i],
1133 i++) {
1134 spaceAddonLeftoverEnd = i;
1135 }
1136 if (spaceAddon > 0 || spaceAddonLeftoverEnd >= 0) {
1137 justificationData = (oldJustficationData != null)
1138 ? oldJustficationData
1139 : new int[END_JUSTIFIABLE + 1];
1140 justificationData[SPACE_ADDON] = spaceAddon;
1141 justificationData[SPACE_ADDON_LEFTOVER_END] =
1142 spaceAddonLeftoverEnd;
1143 justificationData[START_JUSTIFIABLE] =
1144 startJustifiableContent - rowStartOffset;
1145 justificationData[END_JUSTIFIABLE] =
1146 endJustifiableContent - rowStartOffset;
1147 super.layoutMajorAxis(targetSpan, axis, offsets, spans);
1148 }
1149 }
1150
1151 //for justified row we assume the maximum horizontal span
1152 //is MAX_VALUE.
1153 @Override
1154 public float getMaximumSpan(int axis) {
1155 float ret;
1156 if (View.X_AXIS == axis
1157 && isJustifyEnabled()) {
1158 ret = Float.MAX_VALUE;
1159 } else {
1160 ret = super.getMaximumSpan(axis);
1161 }
1162 return ret;
1163 }
1164
1165 /**
1166 * Fetches the child view index representing the given position in
1167 * the model.
1168 *
1169 * @param pos the position >= 0
1170 * @return index of the view representing the given position, or
1171 * -1 if no view represents that position
1172 */
1173 protected int getViewIndexAtPosition(int pos) {
1174 // This is expensive, but are views are not necessarily layed
1175 // out in model order.
1176 if(pos < getStartOffset() || pos >= getEndOffset())
1177 return -1;
1178 for(int counter = getViewCount() - 1; counter >= 0; counter--) {
1179 View v = getView(counter);
1180 if(pos >= v.getStartOffset() &&
1181 pos < v.getEndOffset()) {
1182 return counter;
1183 }
1184 }
1185 return -1;
1186 }
1187
1188 /**
1189 * Gets the left inset.
1190 *
1191 * @return the inset
1192 */
1193 protected short getLeftInset() {
1194 View parentView;
1195 int adjustment = 0;
1196 if ((parentView = getParent()) != null) { //use firstLineIdent for the first row
1197 if (this == parentView.getView(0)) {
1198 adjustment = firstLineIndent;
1199 }
1200 }
1201 return (short)(super.getLeftInset() + adjustment);
1202 }
1203
1204 protected short getBottomInset() {
1205 return (short)(super.getBottomInset() +
1206 ((minorRequest != null) ? minorRequest.preferred : 0) *
1207 lineSpacing);
1208 }
1209
1210 final static int SPACE_ADDON = 0;
1211 final static int SPACE_ADDON_LEFTOVER_END = 1;
1212 final static int START_JUSTIFIABLE = 2;
1213 //this should be the last index in justificationData
1214 final static int END_JUSTIFIABLE = 3;
1215
1216 int justificationData[] = null;
1217 }
1218
1219 }