1 /* 2 * Copyright (c) 2005, 2006, 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 private JTextComponent createPrintShellOnEDT(final JTextComponent textComponent) { 328 assert SwingUtilities.isEventDispatchThread(); 329 330 JTextComponent ret = null; 331 if (textComponent instanceof JPasswordField) { 332 ret = 333 new JPasswordField() { 334 { 335 setEchoChar(((JPasswordField) textComponent).getEchoChar()); 336 setHorizontalAlignment( 337 ((JTextField) textComponent).getHorizontalAlignment()); 338 } 339 @Override 340 public FontMetrics getFontMetrics(Font font) { 341 return (frc.get() == null) 342 ? super.getFontMetrics(font) 343 : FontDesignMetrics.getMetrics(font, frc.get()); 344 } 345 }; 346 } else if (textComponent instanceof JTextField) { 347 ret = 348 new JTextField() { 349 { 350 setHorizontalAlignment( 351 ((JTextField) textComponent).getHorizontalAlignment()); 352 } 353 @Override 354 public FontMetrics getFontMetrics(Font font) { 355 return (frc.get() == null) 356 ? super.getFontMetrics(font) 357 : FontDesignMetrics.getMetrics(font, frc.get()); 358 } 359 }; 360 } else if (textComponent instanceof JTextArea) { 361 ret = 362 new JTextArea() { 363 { 364 JTextArea textArea = (JTextArea) textComponent; 365 setLineWrap(textArea.getLineWrap()); 366 setWrapStyleWord(textArea.getWrapStyleWord()); 367 setTabSize(textArea.getTabSize()); 368 } 369 @Override 370 public FontMetrics getFontMetrics(Font font) { 371 return (frc.get() == null) 372 ? super.getFontMetrics(font) 373 : FontDesignMetrics.getMetrics(font, frc.get()); 374 } 375 }; 376 } else if (textComponent instanceof JTextPane) { 377 ret = 378 new JTextPane() { 379 @Override 380 public FontMetrics getFontMetrics(Font font) { 381 return (frc.get() == null) 382 ? super.getFontMetrics(font) 383 : FontDesignMetrics.getMetrics(font, frc.get()); 384 } 385 @Override 386 public EditorKit getEditorKit() { 387 if (getDocument() == textComponent.getDocument()) { 388 return ((JTextPane) textComponent).getEditorKit(); 389 } else { 390 return super.getEditorKit(); 391 } 392 } 393 }; 394 } else if (textComponent instanceof JEditorPane) { 395 ret = 396 new JEditorPane() { 397 @Override 398 public FontMetrics getFontMetrics(Font font) { 399 return (frc.get() == null) 400 ? super.getFontMetrics(font) 401 : FontDesignMetrics.getMetrics(font, frc.get()); 402 } 403 @Override 404 public EditorKit getEditorKit() { 405 if (getDocument() == textComponent.getDocument()) { 406 return ((JEditorPane) textComponent).getEditorKit(); 407 } else { 408 return super.getEditorKit(); 409 } 410 } 411 }; 412 } 413 //want to occupy the whole width and height by text 414 ret.setBorder(null); 415 416 //set properties from the component to print 417 ret.setOpaque(textComponent.isOpaque()); 418 ret.setEditable(textComponent.isEditable()); 419 ret.setEnabled(textComponent.isEnabled()); 420 ret.setFont(textComponent.getFont()); 421 ret.setBackground(textComponent.getBackground()); 422 ret.setForeground(textComponent.getForeground()); 423 ret.setComponentOrientation( 424 textComponent.getComponentOrientation()); 425 426 if (ret instanceof JEditorPane) { 427 ret.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, 428 textComponent.getClientProperty( 429 JEditorPane.HONOR_DISPLAY_PROPERTIES)); 430 ret.putClientProperty(JEditorPane.W3C_LENGTH_UNITS, 431 textComponent.getClientProperty(JEditorPane.W3C_LENGTH_UNITS)); 432 ret.putClientProperty("charset", 433 textComponent.getClientProperty("charset")); 434 } 435 ret.setDocument(textComponent.getDocument()); 436 return ret; 437 } 438 439 440 441 442 /** 443 * Returns the number of pages in this printable. 444 * <p> 445 * This number is defined only after {@code print} returns NO_SUCH_PAGE. 446 * 447 * @return the number of pages. 448 */ 449 public int getNumberOfPages() { 450 return pagesMetrics.size(); 451 } 452 453 /** 454 * See Printable.print for the API description. 455 * 456 * There are two parts in the implementation. 457 * First part (print) is to be called on the printing thread. 458 * Second part (printOnEDT) is to be called on the EDT only. 459 * 460 * print triggers printOnEDT 461 */ 462 public int print(final Graphics graphics, 463 final PageFormat pf, 464 final int pageIndex) throws PrinterException { 465 if (!isLayouted) { 466 if (graphics instanceof Graphics2D) { 467 frc.set(((Graphics2D)graphics).getFontRenderContext()); 468 } 469 layout((int)Math.floor(pf.getImageableWidth())); 470 calculateRowsMetrics(); 471 } 472 int ret; 473 if (!SwingUtilities.isEventDispatchThread()) { 474 Callable<Integer> doPrintOnEDT = new Callable<Integer>() { 475 public Integer call() throws Exception { 476 return printOnEDT(graphics, pf, pageIndex); 477 } 478 }; 479 FutureTask<Integer> futurePrintOnEDT = 480 new FutureTask<Integer>(doPrintOnEDT); 481 SwingUtilities.invokeLater(futurePrintOnEDT); 482 try { 483 ret = futurePrintOnEDT.get(); 484 } catch (InterruptedException e) { 485 throw new RuntimeException(e); 486 } catch (ExecutionException e) { 487 Throwable cause = e.getCause(); 488 if (cause instanceof PrinterException) { 489 throw (PrinterException)cause; 490 } else if (cause instanceof RuntimeException) { 491 throw (RuntimeException) cause; 492 } else if (cause instanceof Error) { 493 throw (Error) cause; 494 } else { 495 throw new RuntimeException(cause); 496 } 497 } 498 } else { 499 ret = printOnEDT(graphics, pf, pageIndex); 500 } 501 return ret; 502 } 503 504 505 /** 506 * The EDT part of the print method. 507 * 508 * This method is to be called on the EDT only. Layout should be done before 509 * calling this method. 510 */ 511 private int printOnEDT(final Graphics graphics, 512 final PageFormat pf, 513 final int pageIndex) throws PrinterException { 514 assert SwingUtilities.isEventDispatchThread(); 515 Border border = BorderFactory.createEmptyBorder(); 516 //handle header and footer 517 if (headerFormat != null || footerFormat != null) { 518 //Printable page enumeration is 0 base. We need 1 based. 519 Object[] formatArg = new Object[]{Integer.valueOf(pageIndex + 1)}; 520 if (headerFormat != null) { 521 border = new TitledBorder(border, 522 headerFormat.format(formatArg), 523 TitledBorder.CENTER, TitledBorder.ABOVE_TOP, 524 headerFont, printShell.getForeground()); 525 } 526 if (footerFormat != null) { 527 border = new TitledBorder(border, 528 footerFormat.format(formatArg), 529 TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM, 530 footerFont, printShell.getForeground()); 531 } 532 } 533 Insets borderInsets = border.getBorderInsets(printShell); 534 updatePagesMetrics(pageIndex, 535 (int)Math.floor(pf.getImageableHeight()) - borderInsets.top 536 - borderInsets.bottom); 537 538 if (pagesMetrics.size() <= pageIndex) { 539 return NO_SUCH_PAGE; 540 } 541 542 Graphics2D g2d = (Graphics2D)graphics.create(); 543 544 g2d.translate(pf.getImageableX(), pf.getImageableY()); 545 border.paintBorder(printShell, g2d, 0, 0, 546 (int)Math.floor(pf.getImageableWidth()), 547 (int)Math.floor(pf.getImageableHeight())); 548 549 g2d.translate(0, borderInsets.top); 550 //want to clip only vertically 551 Rectangle clip = new Rectangle(0, 0, 552 (int) pf.getWidth(), 553 pagesMetrics.get(pageIndex).end 554 - pagesMetrics.get(pageIndex).start + 1); 555 556 g2d.clip(clip); 557 int xStart = 0; 558 if (ComponentOrientation.RIGHT_TO_LEFT == 559 printShell.getComponentOrientation()) { 560 xStart = (int) pf.getImageableWidth() - printShell.getWidth(); 561 } 562 g2d.translate(xStart, - pagesMetrics.get(pageIndex).start); 563 printShell.print(g2d); 564 565 g2d.dispose(); 566 567 return Printable.PAGE_EXISTS; 568 } 569 570 571 private boolean needReadLock = false; 572 573 /** 574 * Tries to release document's readlock 575 * 576 * Note: Not to be called on the EDT. 577 */ 578 private void releaseReadLock() { 579 assert ! SwingUtilities.isEventDispatchThread(); 580 Document document = textComponentToPrint.getDocument(); 581 if (document instanceof AbstractDocument) { 582 try { 583 ((AbstractDocument) document).readUnlock(); 584 needReadLock = true; 585 } catch (Error ignore) { 586 // readUnlock() might throw StateInvariantError 587 } 588 } 589 } 590 591 592 /** 593 * Tries to acquire document's readLock if it was released 594 * in releaseReadLock() method. 595 * 596 * Note: Not to be called on the EDT. 597 */ 598 private void acquireReadLock() { 599 assert ! SwingUtilities.isEventDispatchThread(); 600 if (needReadLock) { 601 try { 602 /* 603 * wait until all the EDT events are processed 604 * some of the document changes are asynchronous 605 * we need to make sure we get the lock after those changes 606 */ 607 SwingUtilities.invokeAndWait( 608 new Runnable() { 609 public void run() { 610 } 611 }); 612 } catch (InterruptedException ignore) { 613 } catch (java.lang.reflect.InvocationTargetException ignore) { 614 } 615 Document document = textComponentToPrint.getDocument(); 616 ((AbstractDocument) document).readLock(); 617 needReadLock = false; 618 } 619 } 620 621 /** 622 * Prepares {@code printShell} for printing. 623 * 624 * Sets properties from the component to print. 625 * Sets width and FontRenderContext. 626 * 627 * Triggers Views creation for the printShell. 628 * 629 * There are two parts in the implementation. 630 * First part (layout) is to be called on the printing thread. 631 * Second part (layoutOnEDT) is to be called on the EDT only. 632 * 633 * {@code layout} triggers {@code layoutOnEDT}. 634 * 635 * @param width width to layout the text for 636 */ 637 private void layout(final int width) { 638 if (!SwingUtilities.isEventDispatchThread()) { 639 Callable<Object> doLayoutOnEDT = new Callable<Object>() { 640 public Object call() throws Exception { 641 layoutOnEDT(width); 642 return null; 643 } 644 }; 645 FutureTask<Object> futureLayoutOnEDT = new FutureTask<Object>( 646 doLayoutOnEDT); 647 648 /* 649 * We need to release document's readlock while printShell is 650 * initializing 651 */ 652 releaseReadLock(); 653 SwingUtilities.invokeLater(futureLayoutOnEDT); 654 try { 655 futureLayoutOnEDT.get(); 656 } catch (InterruptedException e) { 657 throw new RuntimeException(e); 658 } catch (ExecutionException e) { 659 Throwable cause = e.getCause(); 660 if (cause instanceof RuntimeException) { 661 throw (RuntimeException) cause; 662 } else if (cause instanceof Error) { 663 throw (Error) cause; 664 } else { 665 throw new RuntimeException(cause); 666 } 667 } finally { 668 acquireReadLock(); 669 } 670 } else { 671 layoutOnEDT(width); 672 } 673 674 isLayouted = true; 675 } 676 677 /** 678 * The EDT part of layout method. 679 * 680 * This method is to be called on the EDT only. 681 */ 682 private void layoutOnEDT(final int width) { 683 assert SwingUtilities.isEventDispatchThread(); 684 //need to have big value but smaller than MAX_VALUE otherwise 685 //printing goes south due to overflow somewhere 686 final int HUGE_INTEGER = Integer.MAX_VALUE - 1000; 687 688 CellRendererPane rendererPane = new CellRendererPane(); 689 690 //need to use JViewport since text is layouted to the viewPort width 691 //otherwise it will be layouted to the maximum text width 692 JViewport viewport = new JViewport(); 693 viewport.setBorder(null); 694 Dimension size = new Dimension(width, HUGE_INTEGER); 695 696 //JTextField is a special case 697 //it layouts text in the middle by Y 698 if (printShell instanceof JTextField) { 699 size = 700 new Dimension(size.width, printShell.getPreferredSize().height); 701 } 702 printShell.setSize(size); 703 viewport.setComponentOrientation(printShell.getComponentOrientation()); 704 viewport.setSize(size); 705 viewport.add(printShell); 706 rendererPane.add(viewport); 707 } 708 709 /** 710 * Calculates pageMetrics for the pages up to the {@code pageIndex} using 711 * {@code rowsMetrics}. 712 * Metrics are stored in the {@code pagesMetrics}. 713 * 714 * @param pageIndex the page to update the metrics for 715 * @param pageHeight the page height 716 */ 717 private void updatePagesMetrics(final int pageIndex, final int pageHeight) { 718 while (pageIndex >= pagesMetrics.size() && !rowsMetrics.isEmpty()) { 719 // add one page to the pageMetrics 720 int lastPage = pagesMetrics.size() - 1; 721 int pageStart = (lastPage >= 0) 722 ? pagesMetrics.get(lastPage).end + 1 723 : 0; 724 int rowIndex; 725 for (rowIndex = 0; 726 rowIndex < rowsMetrics.size() 727 && (rowsMetrics.get(rowIndex).end - pageStart + 1) 728 <= pageHeight; 729 rowIndex++) { 730 } 731 if (rowIndex == 0) { 732 // can not fit a single row 733 // need to split 734 pagesMetrics.add( 735 new IntegerSegment(pageStart, pageStart + pageHeight - 1)); 736 } else { 737 rowIndex--; 738 pagesMetrics.add(new IntegerSegment(pageStart, 739 rowsMetrics.get(rowIndex).end)); 740 for (int i = 0; i <= rowIndex; i++) { 741 rowsMetrics.remove(0); 742 } 743 } 744 } 745 } 746 747 /** 748 * Calculates rowsMetrics for the document. The result is stored 749 * in the {@code rowsMetrics}. 750 * 751 * Two steps process. 752 * First step is to find yStart and yEnd for the every document position. 753 * Second step is to merge all intersected segments ( [yStart, yEnd] ). 754 */ 755 private void calculateRowsMetrics() { 756 final int documentLength = printShell.getDocument().getLength(); 757 List<IntegerSegment> documentMetrics = new ArrayList<IntegerSegment>(LIST_SIZE); 758 Rectangle rect; 759 for (int i = 0, previousY = -1, previousHeight = -1; i < documentLength; 760 i++) { 761 try { 762 rect = printShell.modelToView(i); 763 if (rect != null) { 764 int y = (int) rect.getY(); 765 int height = (int) rect.getHeight(); 766 if (height != 0 767 && (y != previousY || height != previousHeight)) { 768 /* 769 * we do not store the same value as previous. in our 770 * documents it is often for consequent positons to have 771 * the same modelToView y and height. 772 */ 773 previousY = y; 774 previousHeight = height; 775 documentMetrics.add(new IntegerSegment(y, y + height - 1)); 776 } 777 } 778 } catch (BadLocationException e) { 779 assert false; 780 } 781 } 782 /* 783 * Merge all intersected segments. 784 */ 785 Collections.sort(documentMetrics); 786 int yStart = Integer.MIN_VALUE; 787 int yEnd = Integer.MIN_VALUE; 788 for (IntegerSegment segment : documentMetrics) { 789 if (yEnd < segment.start) { 790 if (yEnd != Integer.MIN_VALUE) { 791 rowsMetrics.add(new IntegerSegment(yStart, yEnd)); 792 } 793 yStart = segment.start; 794 yEnd = segment.end; 795 } else { 796 yEnd = segment.end; 797 } 798 } 799 if (yEnd != Integer.MIN_VALUE) { 800 rowsMetrics.add(new IntegerSegment(yStart, yEnd)); 801 } 802 } 803 804 /** 805 * Class to represent segment of integers. 806 * we do not call it Segment to avoid confusion with 807 * javax.swing.text.Segment 808 */ 809 private static class IntegerSegment implements Comparable<IntegerSegment> { 810 final int start; 811 final int end; 812 813 IntegerSegment(int start, int end) { 814 this.start = start; 815 this.end = end; 816 } 817 818 public int compareTo(IntegerSegment object) { 819 int startsDelta = start - object.start; 820 return (startsDelta != 0) ? startsDelta : end - object.end; 821 } 822 823 @Override 824 public boolean equals(Object obj) { 825 if (obj instanceof IntegerSegment) { 826 return compareTo((IntegerSegment) obj) == 0; 827 } else { 828 return false; 829 } 830 } 831 832 @Override 833 public int hashCode() { 834 // from the "Effective Java: Programming Language Guide" 835 int result = 17; 836 result = 37 * result + start; 837 result = 37 * result + end; 838 return result; 839 } 840 841 @Override 842 public String toString() { 843 return "IntegerSegment [" + start + ", " + end + "]"; 844 } 845 } 846 }