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