1 /* 2 * Copyright (c) 2005, 2014, 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 sun.swing.text; 26 27 import java.awt.ComponentOrientation; 28 import java.awt.Dimension; 29 import java.awt.Font; 30 import java.awt.FontMetrics; 31 import java.awt.Graphics; 32 import java.awt.Graphics2D; 33 import java.awt.Insets; 34 import java.awt.Rectangle; 35 import java.awt.Component; 36 import java.awt.Container; 37 import java.awt.font.FontRenderContext; 38 import java.awt.print.PageFormat; 39 import java.awt.print.Printable; 40 import java.awt.print.PrinterException; 41 import java.text.MessageFormat; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.concurrent.Callable; 46 import java.util.concurrent.ExecutionException; 47 import java.util.concurrent.FutureTask; 48 import java.util.concurrent.atomic.AtomicReference; 49 50 import javax.swing.*; 51 import javax.swing.border.Border; 52 import javax.swing.border.TitledBorder; 53 import javax.swing.text.BadLocationException; 54 import javax.swing.text.JTextComponent; 55 import javax.swing.text.Document; 56 import javax.swing.text.EditorKit; 57 import javax.swing.text.AbstractDocument; 58 import javax.swing.text.html.HTMLDocument; 59 import javax.swing.text.html.HTML; 60 61 import sun.font.FontDesignMetrics; 62 63 import sun.swing.text.html.FrameEditorPaneTag; 64 65 /** 66 * An implementation of {@code Printable} to print {@code JTextComponent} with 67 * the header and footer. 68 * 69 * <h1> 70 * WARNING: this class is to be used in 71 * javax.swing.text.JTextComponent only. 72 * </h1> 73 * 74 * <p> 75 * The implementation creates a new {@code JTextComponent} ({@code printShell}) 76 * to print the content using the {@code Document}, {@code EditorKit} and 77 * rendering-affecting properties from the original {@code JTextComponent}. 78 * 79 * <p> 80 * {@code printShell} is laid out on the first {@code print} invocation. 81 * 82 * <p> 83 * This class can be used on any thread. Part of the implementation is executed 84 * on the EDT though. 85 * 86 * @author Igor Kushnirskiy 87 * 88 * @since 1.6 89 */ 90 public class TextComponentPrintable implements CountingPrintable { 91 92 93 private static final int LIST_SIZE = 1000; 94 95 private boolean isLayouted = false; 96 97 /* 98 * The text component to print. 99 */ 100 private final JTextComponent textComponentToPrint; 101 102 /* 103 * The FontRenderContext to layout and print with 104 */ 105 private final AtomicReference<FontRenderContext> frc = 106 new AtomicReference<FontRenderContext>(null); 107 108 /** 109 * Special text component used to print to the printer. 110 */ 111 private final JTextComponent printShell; 112 113 private final MessageFormat headerFormat; 114 private final MessageFormat footerFormat; 115 116 private static final float HEADER_FONT_SIZE = 18.0f; 117 private static final float FOOTER_FONT_SIZE = 12.0f; 118 119 private final Font headerFont; 120 private final Font footerFont; 121 122 /** 123 * stores metrics for the unhandled rows. The only metrics we need are 124 * yStart and yEnd when row is handled by updatePagesMetrics it is removed 125 * from the list. Thus the head of the list is the fist row to handle. 126 * 127 * sorted 128 */ 129 private final List<IntegerSegment> rowsMetrics; 130 131 /** 132 * thread-safe list for storing pages metrics. The only metrics we need are 133 * yStart and yEnd. 134 * It has to be thread-safe since metrics are calculated on 135 * the printing thread and accessed on the EDT thread. 136 * 137 * sorted 138 */ 139 private final List<IntegerSegment> pagesMetrics; 140 141 /** 142 * Returns {@code TextComponentPrintable} to print {@code textComponent}. 143 * 144 * @param textComponent {@code JTextComponent} to print 145 * @param headerFormat the page header, or {@code null} for none 146 * @param footerFormat the page footer, or {@code null} for none 147 * @return {@code TextComponentPrintable} to print {@code textComponent} 148 */ 149 public static Printable getPrintable(final JTextComponent textComponent, 150 final MessageFormat headerFormat, 151 final MessageFormat footerFormat) { 152 153 if (textComponent instanceof JEditorPane 154 && isFrameSetDocument(textComponent.getDocument())) { 155 //for document with frames we create one printable per 156 //frame and merge them with the CompoundPrintable. 157 List<JEditorPane> frames = getFrames((JEditorPane) textComponent); 158 List<CountingPrintable> printables = 159 new ArrayList<CountingPrintable>(); 160 for (JEditorPane frame : frames) { 161 printables.add((CountingPrintable) 162 getPrintable(frame, headerFormat, footerFormat)); 163 } 164 return new CompoundPrintable(printables); 165 } else { 166 return new TextComponentPrintable(textComponent, 167 headerFormat, footerFormat); 168 } 169 } 170 171 /** 172 * Checks whether the document has frames. Only HTMLDocument might 173 * have frames. 174 * 175 * @param document the {@code Document} to check 176 * @return {@code true} if the {@code document} has frames 177 */ 178 private static boolean isFrameSetDocument(final Document document) { 179 boolean ret = false; 180 if (document instanceof HTMLDocument) { 181 HTMLDocument htmlDocument = (HTMLDocument)document; 182 if (htmlDocument.getIterator(HTML.Tag.FRAME).isValid()) { 183 ret = true; 184 } 185 } 186 return ret; 187 } 188 189 190 /** 191 * Returns frames under the {@code editor}. 192 * The frames are created if necessary. 193 * 194 * @param editor the {@JEditorPane} to find the frames for 195 * @return list of all frames 196 */ 197 private static List<JEditorPane> getFrames(final JEditorPane editor) { 198 List<JEditorPane> list = new ArrayList<JEditorPane>(); 199 getFrames(editor, list); 200 if (list.size() == 0) { 201 //the frames have not been created yet. 202 //let's trigger the frames creation. 203 createFrames(editor); 204 getFrames(editor, list); 205 } 206 return list; 207 } 208 209 /** 210 * Adds all {@code JEditorPanes} under {@code container} tagged by {@code 211 * FrameEditorPaneTag} to the {@code list}. It adds only top 212 * level {@code JEditorPanes}. For instance if there is a frame 213 * inside the frame it will return the top frame only. 214 * 215 * @param c the container to find all frames under 216 * @param list {@code List} to append the results too 217 */ 218 private static void getFrames(final Container container, List<JEditorPane> list) { 219 for (Component c : container.getComponents()) { 220 if (c instanceof FrameEditorPaneTag 221 && c instanceof JEditorPane ) { //it should be always JEditorPane 222 list.add((JEditorPane) c); 223 } else { 224 if (c instanceof Container) { 225 getFrames((Container) c, list); 226 } 227 } 228 } 229 } 230 231 /** 232 * Triggers the frames creation for {@code JEditorPane} 233 * 234 * @param editor the {@code JEditorPane} to create frames for 235 */ 236 private static void createFrames(final JEditorPane editor) { 237 Runnable doCreateFrames = 238 new Runnable() { 239 public void run() { 240 final int WIDTH = 500; 241 final int HEIGHT = 500; 242 CellRendererPane rendererPane = new CellRendererPane(); 243 rendererPane.add(editor); 244 //the values do not matter 245 //we only need to get frames created 246 rendererPane.setSize(WIDTH, HEIGHT); 247 }; 248 }; 249 if (SwingUtilities.isEventDispatchThread()) { 250 doCreateFrames.run(); 251 } else { 252 try { 253 SwingUtilities.invokeAndWait(doCreateFrames); 254 } catch (Exception e) { 255 if (e instanceof RuntimeException) { 256 throw (RuntimeException) e; 257 } else { 258 throw new RuntimeException(e); 259 } 260 } 261 } 262 } 263 264 /** 265 * Constructs {@code TextComponentPrintable} to print {@code JTextComponent} 266 * {@code textComponent} with {@code headerFormat} and {@code footerFormat}. 267 * 268 * @param textComponent {@code JTextComponent} to print 269 * @param headerFormat the page header or {@code null} for none 270 * @param footerFormat the page footer or {@code null} for none 271 */ 272 private TextComponentPrintable(JTextComponent textComponent, 273 MessageFormat headerFormat, 274 MessageFormat footerFormat) { 275 this.textComponentToPrint = textComponent; 276 this.headerFormat = headerFormat; 277 this.footerFormat = footerFormat; 278 headerFont = textComponent.getFont().deriveFont(Font.BOLD, 279 HEADER_FONT_SIZE); 280 footerFont = textComponent.getFont().deriveFont(Font.PLAIN, 281 FOOTER_FONT_SIZE); 282 this.pagesMetrics = 283 Collections.synchronizedList(new ArrayList<IntegerSegment>()); 284 this.rowsMetrics = new ArrayList<IntegerSegment>(LIST_SIZE); 285 this.printShell = createPrintShell(textComponent); 286 } 287 288 289 /** 290 * creates a printShell. 291 * It creates closest text component to {@code textComponent} 292 * which uses {@code frc} from the {@code TextComponentPrintable} 293 * for the {@code getFontMetrics} method. 294 * 295 * @param textComponent {@code JTextComponent} to create a 296 * printShell for 297 * @return the print shell 298 */ 299 private JTextComponent createPrintShell(final JTextComponent textComponent) { 300 if (SwingUtilities.isEventDispatchThread()) { 301 return createPrintShellOnEDT(textComponent); 302 } else { 303 FutureTask<JTextComponent> futureCreateShell = 304 new FutureTask<JTextComponent>( 305 new Callable<JTextComponent>() { 306 public JTextComponent call() throws Exception { 307 return createPrintShellOnEDT(textComponent); 308 } 309 }); 310 SwingUtilities.invokeLater(futureCreateShell); 311 try { 312 return futureCreateShell.get(); 313 } catch (InterruptedException e) { 314 throw new RuntimeException(e); 315 } catch (ExecutionException e) { 316 Throwable cause = e.getCause(); 317 if (cause instanceof Error) { 318 throw (Error) cause; 319 } 320 if (cause instanceof RuntimeException) { 321 throw (RuntimeException) cause; 322 } 323 throw new AssertionError(cause); 324 } 325 } 326 } 327 @SuppressWarnings("serial") // anonymous class inside 328 private JTextComponent createPrintShellOnEDT(final JTextComponent textComponent) { 329 assert SwingUtilities.isEventDispatchThread(); 330 331 JTextComponent ret = null; 332 if (textComponent instanceof JPasswordField) { 333 ret = 334 new JPasswordField() { 335 { 336 setEchoChar(((JPasswordField) textComponent).getEchoChar()); 337 setHorizontalAlignment( 338 ((JTextField) textComponent).getHorizontalAlignment()); 339 } 340 @Override 341 public FontMetrics getFontMetrics(Font font) { 342 return (frc.get() == null) 343 ? super.getFontMetrics(font) 344 : FontDesignMetrics.getMetrics(font, frc.get()); 345 } 346 }; 347 } else if (textComponent instanceof JTextField) { 348 ret = 349 new JTextField() { 350 { 351 setHorizontalAlignment( 352 ((JTextField) textComponent).getHorizontalAlignment()); 353 } 354 @Override 355 public FontMetrics getFontMetrics(Font font) { 356 return (frc.get() == null) 357 ? super.getFontMetrics(font) 358 : FontDesignMetrics.getMetrics(font, frc.get()); 359 } 360 }; 361 } else if (textComponent instanceof JTextArea) { 362 ret = 363 new JTextArea() { 364 { 365 JTextArea textArea = (JTextArea) textComponent; 366 setLineWrap(textArea.getLineWrap()); 367 setWrapStyleWord(textArea.getWrapStyleWord()); 368 setTabSize(textArea.getTabSize()); 369 } 370 @Override 371 public FontMetrics getFontMetrics(Font font) { 372 return (frc.get() == null) 373 ? super.getFontMetrics(font) 374 : FontDesignMetrics.getMetrics(font, frc.get()); 375 } 376 }; 377 } else if (textComponent instanceof JTextPane) { 378 ret = 379 new JTextPane() { 380 @Override 381 public FontMetrics getFontMetrics(Font font) { 382 return (frc.get() == null) 383 ? super.getFontMetrics(font) 384 : FontDesignMetrics.getMetrics(font, frc.get()); 385 } 386 @Override 387 public EditorKit getEditorKit() { 388 if (getDocument() == textComponent.getDocument()) { 389 return ((JTextPane) textComponent).getEditorKit(); 390 } else { 391 return super.getEditorKit(); 392 } 393 } 394 }; 395 } else if (textComponent instanceof JEditorPane) { 396 ret = 397 new JEditorPane() { 398 @Override 399 public FontMetrics getFontMetrics(Font font) { 400 return (frc.get() == null) 401 ? super.getFontMetrics(font) 402 : FontDesignMetrics.getMetrics(font, frc.get()); 403 } 404 @Override 405 public EditorKit getEditorKit() { 406 if (getDocument() == textComponent.getDocument()) { 407 return ((JEditorPane) textComponent).getEditorKit(); 408 } else { 409 return super.getEditorKit(); 410 } 411 } 412 }; 413 } 414 //want to occupy the whole width and height by text 415 ret.setBorder(null); 416 417 //set properties from the component to print 418 ret.setOpaque(textComponent.isOpaque()); 419 ret.setEditable(textComponent.isEditable()); 420 ret.setEnabled(textComponent.isEnabled()); 421 ret.setFont(textComponent.getFont()); 422 ret.setBackground(textComponent.getBackground()); 423 ret.setForeground(textComponent.getForeground()); 424 ret.setComponentOrientation( 425 textComponent.getComponentOrientation()); 426 427 if (ret instanceof JEditorPane) { 428 ret.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, 429 textComponent.getClientProperty( 430 JEditorPane.HONOR_DISPLAY_PROPERTIES)); 431 ret.putClientProperty(JEditorPane.W3C_LENGTH_UNITS, 432 textComponent.getClientProperty(JEditorPane.W3C_LENGTH_UNITS)); 433 ret.putClientProperty("charset", 434 textComponent.getClientProperty("charset")); 435 } 436 ret.setDocument(textComponent.getDocument()); 437 return ret; 438 } 439 440 441 442 443 /** 444 * Returns the number of pages in this printable. 445 * <p> 446 * This number is defined only after {@code print} returns NO_SUCH_PAGE. 447 * 448 * @return the number of pages. 449 */ 450 public int getNumberOfPages() { 451 return pagesMetrics.size(); 452 } 453 454 /** 455 * See Printable.print for the API description. 456 * 457 * There are two parts in the implementation. 458 * First part (print) is to be called on the printing thread. 459 * Second part (printOnEDT) is to be called on the EDT only. 460 * 461 * print triggers printOnEDT 462 */ 463 public int print(final Graphics graphics, 464 final PageFormat pf, 465 final int pageIndex) throws PrinterException { 466 if (!isLayouted) { 467 if (graphics instanceof Graphics2D) { 468 frc.set(((Graphics2D)graphics).getFontRenderContext()); 469 } 470 layout((int)Math.floor(pf.getImageableWidth())); 471 calculateRowsMetrics(); 472 } 473 int ret; 474 if (!SwingUtilities.isEventDispatchThread()) { 475 Callable<Integer> doPrintOnEDT = new Callable<Integer>() { 476 public Integer call() throws Exception { 477 return printOnEDT(graphics, pf, pageIndex); 478 } 479 }; 480 FutureTask<Integer> futurePrintOnEDT = 481 new FutureTask<Integer>(doPrintOnEDT); 482 SwingUtilities.invokeLater(futurePrintOnEDT); 483 try { 484 ret = futurePrintOnEDT.get(); 485 } catch (InterruptedException e) { 486 throw new RuntimeException(e); 487 } catch (ExecutionException e) { 488 Throwable cause = e.getCause(); 489 if (cause instanceof PrinterException) { 490 throw (PrinterException)cause; 491 } else if (cause instanceof RuntimeException) { 492 throw (RuntimeException) cause; 493 } else if (cause instanceof Error) { 494 throw (Error) cause; 495 } else { 496 throw new RuntimeException(cause); 497 } 498 } 499 } else { 500 ret = printOnEDT(graphics, pf, pageIndex); 501 } 502 return ret; 503 } 504 505 506 /** 507 * The EDT part of the print method. 508 * 509 * This method is to be called on the EDT only. Layout should be done before 510 * calling this method. 511 */ 512 private int printOnEDT(final Graphics graphics, 513 final PageFormat pf, 514 final int pageIndex) throws PrinterException { 515 assert SwingUtilities.isEventDispatchThread(); 516 Border border = BorderFactory.createEmptyBorder(); 517 //handle header and footer 518 if (headerFormat != null || footerFormat != null) { 519 //Printable page enumeration is 0 base. We need 1 based. 520 Object[] formatArg = new Object[]{Integer.valueOf(pageIndex + 1)}; 521 if (headerFormat != null) { 522 border = new TitledBorder(border, 523 headerFormat.format(formatArg), 524 TitledBorder.CENTER, TitledBorder.ABOVE_TOP, 525 headerFont, printShell.getForeground()); 526 } 527 if (footerFormat != null) { 528 border = new TitledBorder(border, 529 footerFormat.format(formatArg), 530 TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM, 531 footerFont, printShell.getForeground()); 532 } 533 } 534 Insets borderInsets = border.getBorderInsets(printShell); 535 updatePagesMetrics(pageIndex, 536 (int)Math.floor(pf.getImageableHeight()) - borderInsets.top 537 - borderInsets.bottom); 538 539 if (pagesMetrics.size() <= pageIndex) { 540 return NO_SUCH_PAGE; 541 } 542 543 Graphics2D g2d = (Graphics2D)graphics.create(); 544 545 g2d.translate(pf.getImageableX(), pf.getImageableY()); 546 border.paintBorder(printShell, g2d, 0, 0, 547 (int)Math.floor(pf.getImageableWidth()), 548 (int)Math.floor(pf.getImageableHeight())); 549 550 g2d.translate(0, borderInsets.top); 551 //want to clip only vertically 552 Rectangle clip = new Rectangle(0, 0, 553 (int) pf.getWidth(), 554 pagesMetrics.get(pageIndex).end 555 - pagesMetrics.get(pageIndex).start + 1); 556 557 g2d.clip(clip); 558 int xStart = 0; 559 if (ComponentOrientation.RIGHT_TO_LEFT == 560 printShell.getComponentOrientation()) { 561 xStart = (int) pf.getImageableWidth() - printShell.getWidth(); 562 } 563 g2d.translate(xStart, - pagesMetrics.get(pageIndex).start); 564 printShell.print(g2d); 565 566 g2d.dispose(); 567 568 return Printable.PAGE_EXISTS; 569 } 570 571 572 private boolean needReadLock = false; 573 574 /** 575 * Tries to release document's readlock 576 * 577 * Note: Not to be called on the EDT. 578 */ 579 private void releaseReadLock() { 580 assert ! SwingUtilities.isEventDispatchThread(); 581 Document document = textComponentToPrint.getDocument(); 582 if (document instanceof AbstractDocument) { 583 try { 584 ((AbstractDocument) document).readUnlock(); 585 needReadLock = true; 586 } catch (Error ignore) { 587 // readUnlock() might throw StateInvariantError 588 } 589 } 590 } 591 592 593 /** 594 * Tries to acquire document's readLock if it was released 595 * in releaseReadLock() method. 596 * 597 * Note: Not to be called on the EDT. 598 */ 599 private void acquireReadLock() { 600 assert ! SwingUtilities.isEventDispatchThread(); 601 if (needReadLock) { 602 try { 603 /* 604 * wait until all the EDT events are processed 605 * some of the document changes are asynchronous 606 * we need to make sure we get the lock after those changes 607 */ 608 SwingUtilities.invokeAndWait( 609 new Runnable() { 610 public void run() { 611 } 612 }); 613 } catch (InterruptedException ignore) { 614 } catch (java.lang.reflect.InvocationTargetException ignore) { 615 } 616 Document document = textComponentToPrint.getDocument(); 617 ((AbstractDocument) document).readLock(); 618 needReadLock = false; 619 } 620 } 621 622 /** 623 * Prepares {@code printShell} for printing. 624 * 625 * Sets properties from the component to print. 626 * Sets width and FontRenderContext. 627 * 628 * Triggers Views creation for the printShell. 629 * 630 * There are two parts in the implementation. 631 * First part (layout) is to be called on the printing thread. 632 * Second part (layoutOnEDT) is to be called on the EDT only. 633 * 634 * {@code layout} triggers {@code layoutOnEDT}. 635 * 636 * @param width width to layout the text for 637 */ 638 private void layout(final int width) { 639 if (!SwingUtilities.isEventDispatchThread()) { 640 Callable<Object> doLayoutOnEDT = new Callable<Object>() { 641 public Object call() throws Exception { 642 layoutOnEDT(width); 643 return null; 644 } 645 }; 646 FutureTask<Object> futureLayoutOnEDT = new FutureTask<Object>( 647 doLayoutOnEDT); 648 649 /* 650 * We need to release document's readlock while printShell is 651 * initializing 652 */ 653 releaseReadLock(); 654 SwingUtilities.invokeLater(futureLayoutOnEDT); 655 try { 656 futureLayoutOnEDT.get(); 657 } catch (InterruptedException e) { 658 throw new RuntimeException(e); 659 } catch (ExecutionException e) { 660 Throwable cause = e.getCause(); 661 if (cause instanceof RuntimeException) { 662 throw (RuntimeException) cause; 663 } else if (cause instanceof Error) { 664 throw (Error) cause; 665 } else { 666 throw new RuntimeException(cause); 667 } 668 } finally { 669 acquireReadLock(); 670 } 671 } else { 672 layoutOnEDT(width); 673 } 674 675 isLayouted = true; 676 } 677 678 /** 679 * The EDT part of layout method. 680 * 681 * This method is to be called on the EDT only. 682 */ 683 private void layoutOnEDT(final int width) { 684 assert SwingUtilities.isEventDispatchThread(); 685 //need to have big value but smaller than MAX_VALUE otherwise 686 //printing goes south due to overflow somewhere 687 final int HUGE_INTEGER = Integer.MAX_VALUE - 1000; 688 689 CellRendererPane rendererPane = new CellRendererPane(); 690 691 //need to use JViewport since text is layouted to the viewPort width 692 //otherwise it will be layouted to the maximum text width 693 JViewport viewport = new JViewport(); 694 viewport.setBorder(null); 695 Dimension size = new Dimension(width, HUGE_INTEGER); 696 697 //JTextField is a special case 698 //it layouts text in the middle by Y 699 if (printShell instanceof JTextField) { 700 size = 701 new Dimension(size.width, printShell.getPreferredSize().height); 702 } 703 printShell.setSize(size); 704 viewport.setComponentOrientation(printShell.getComponentOrientation()); 705 viewport.setSize(size); 706 viewport.add(printShell); 707 rendererPane.add(viewport); 708 } 709 710 /** 711 * Calculates pageMetrics for the pages up to the {@code pageIndex} using 712 * {@code rowsMetrics}. 713 * Metrics are stored in the {@code pagesMetrics}. 714 * 715 * @param pageIndex the page to update the metrics for 716 * @param pageHeight the page height 717 */ 718 private void updatePagesMetrics(final int pageIndex, final int pageHeight) { 719 while (pageIndex >= pagesMetrics.size() && !rowsMetrics.isEmpty()) { 720 // add one page to the pageMetrics 721 int lastPage = pagesMetrics.size() - 1; 722 int pageStart = (lastPage >= 0) 723 ? pagesMetrics.get(lastPage).end + 1 724 : 0; 725 int rowIndex; 726 for (rowIndex = 0; 727 rowIndex < rowsMetrics.size() 728 && (rowsMetrics.get(rowIndex).end - pageStart + 1) 729 <= pageHeight; 730 rowIndex++) { 731 } 732 if (rowIndex == 0) { 733 // can not fit a single row 734 // need to split 735 pagesMetrics.add( 736 new IntegerSegment(pageStart, pageStart + pageHeight - 1)); 737 } else { 738 rowIndex--; 739 pagesMetrics.add(new IntegerSegment(pageStart, 740 rowsMetrics.get(rowIndex).end)); 741 for (int i = 0; i <= rowIndex; i++) { 742 rowsMetrics.remove(0); 743 } 744 } 745 } 746 } 747 748 /** 749 * Calculates rowsMetrics for the document. The result is stored 750 * in the {@code rowsMetrics}. 751 * 752 * Two steps process. 753 * First step is to find yStart and yEnd for the every document position. 754 * Second step is to merge all intersected segments ( [yStart, yEnd] ). 755 */ 756 private void calculateRowsMetrics() { 757 final int documentLength = printShell.getDocument().getLength(); 758 List<IntegerSegment> documentMetrics = new ArrayList<IntegerSegment>(LIST_SIZE); 759 Rectangle rect; 760 for (int i = 0, previousY = -1, previousHeight = -1; i < documentLength; 761 i++) { 762 try { 763 rect = printShell.modelToView(i); 764 if (rect != null) { 765 int y = (int) rect.getY(); 766 int height = (int) rect.getHeight(); 767 if (height != 0 768 && (y != previousY || height != previousHeight)) { 769 /* 770 * we do not store the same value as previous. in our 771 * documents it is often for consequent positons to have 772 * the same modelToView y and height. 773 */ 774 previousY = y; 775 previousHeight = height; 776 documentMetrics.add(new IntegerSegment(y, y + height - 1)); 777 } 778 } 779 } catch (BadLocationException e) { 780 assert false; 781 } 782 } 783 /* 784 * Merge all intersected segments. 785 */ 786 Collections.sort(documentMetrics); 787 int yStart = Integer.MIN_VALUE; 788 int yEnd = Integer.MIN_VALUE; 789 for (IntegerSegment segment : documentMetrics) { 790 if (yEnd < segment.start) { 791 if (yEnd != Integer.MIN_VALUE) { 792 rowsMetrics.add(new IntegerSegment(yStart, yEnd)); 793 } 794 yStart = segment.start; 795 yEnd = segment.end; 796 } else { 797 yEnd = segment.end; 798 } 799 } 800 if (yEnd != Integer.MIN_VALUE) { 801 rowsMetrics.add(new IntegerSegment(yStart, yEnd)); 802 } 803 } 804 805 /** 806 * Class to represent segment of integers. 807 * we do not call it Segment to avoid confusion with 808 * javax.swing.text.Segment 809 */ 810 private static class IntegerSegment implements Comparable<IntegerSegment> { 811 final int start; 812 final int end; 813 814 IntegerSegment(int start, int end) { 815 this.start = start; 816 this.end = end; 817 } 818 819 public int compareTo(IntegerSegment object) { 820 int startsDelta = start - object.start; 821 return (startsDelta != 0) ? startsDelta : end - object.end; 822 } 823 824 @Override 825 public boolean equals(Object obj) { 826 if (obj instanceof IntegerSegment) { 827 return compareTo((IntegerSegment) obj) == 0; 828 } else { 829 return false; 830 } 831 } 832 833 @Override 834 public int hashCode() { 835 // from the "Effective Java: Programming Language Guide" 836 int result = 17; 837 result = 37 * result + start; 838 result = 37 * result + end; 839 return result; 840 } 841 842 @Override 843 public String toString() { 844 return "IntegerSegment [" + start + ", " + end + "]"; 845 } 846 } 847 }