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 }