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 }