| /* | |
| * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package sun.swing.text; | |
| import java.awt.ComponentOrientation; | |
| import java.awt.Dimension; | |
| import java.awt.Font; | |
| import java.awt.FontMetrics; | |
| import java.awt.Graphics; | |
| import java.awt.Graphics2D; | |
| import java.awt.Insets; | |
| import java.awt.Rectangle; | |
| import java.awt.Component; | |
| import java.awt.Container; | |
| import java.awt.font.FontRenderContext; | |
| import java.awt.print.PageFormat; | |
| import java.awt.print.Printable; | |
| import java.awt.print.PrinterException; | |
| import java.lang.reflect.InvocationTargetException; | |
| import java.text.MessageFormat; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.concurrent.Callable; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.FutureTask; | |
| import javax.swing.BorderFactory; | |
| import javax.swing.CellRendererPane; | |
| import javax.swing.JEditorPane; | |
| import javax.swing.JPasswordField; | |
| import javax.swing.JTextArea; | |
| import javax.swing.JTextField; | |
| import javax.swing.JTextPane; | |
| import javax.swing.JViewport; | |
| import javax.swing.SwingUtilities; | |
| import javax.swing.border.Border; | |
| import javax.swing.border.TitledBorder; | |
| import javax.swing.text.BadLocationException; | |
| import javax.swing.text.JTextComponent; | |
| import javax.swing.text.Document; | |
| import javax.swing.text.EditorKit; | |
| import javax.swing.text.AbstractDocument; | |
| import javax.swing.text.html.HTMLDocument; | |
| import javax.swing.text.html.HTML; | |
| import sun.font.FontDesignMetrics; | |
| import sun.swing.text.html.FrameEditorPaneTag; | |
| /** | |
| * An implementation of {@code Printable} to print {@code JTextComponent} with | |
| * the header and footer. | |
| * | |
| * <h1> | |
| * WARNING: this class is to be used in | |
| * javax.swing.text.JTextComponent only. | |
| * </h1> | |
| * | |
| * <p> | |
| * The implementation creates a new {@code JTextComponent} ({@code printShell}) | |
| * to print the content using the {@code Document}, {@code EditorKit} and | |
| * rendering-affecting properties from the original {@code JTextComponent}. | |
| * | |
| * <p> | |
| * {@code printShell} is laid out on the first {@code print} invocation. | |
| * | |
| * <p> | |
| * This class can be used on any thread. Part of the implementation is executed | |
| * on the EDT though. | |
| * | |
| * @author Igor Kushnirskiy | |
| * | |
| * @since 1.6 | |
| */ | |
| public class TextComponentPrintable implements CountingPrintable { | |
| private static final int LIST_SIZE = 1000; | |
| private boolean isLayouted = false; | |
| /* | |
| * The text component to print. | |
| */ | |
| private final JTextComponent textComponentToPrint; | |
| /* | |
| * The FontRenderContext to layout and print with | |
| */ | |
| private volatile FontRenderContext frc; | |
| /** | |
| * Special text component used to print to the printer. | |
| */ | |
| private final JTextComponent printShell; | |
| private final MessageFormat headerFormat; | |
| private final MessageFormat footerFormat; | |
| private static final float HEADER_FONT_SIZE = 18.0f; | |
| private static final float FOOTER_FONT_SIZE = 12.0f; | |
| private final Font headerFont; | |
| private final Font footerFont; | |
| /** | |
| * stores metrics for the unhandled rows. The only metrics we need are | |
| * yStart and yEnd when row is handled by updatePagesMetrics it is removed | |
| * from the list. Thus the head of the list is the fist row to handle. | |
| * | |
| * sorted | |
| */ | |
| private final List<IntegerSegment> rowsMetrics; | |
| /** | |
| * thread-safe list for storing pages metrics. The only metrics we need are | |
| * yStart and yEnd. | |
| * It has to be thread-safe since metrics are calculated on | |
| * the printing thread and accessed on the EDT thread. | |
| * | |
| * sorted | |
| */ | |
| private final List<IntegerSegment> pagesMetrics; | |
| /** | |
| * Returns {@code TextComponentPrintable} to print {@code textComponent}. | |
| * | |
| * @param textComponent {@code JTextComponent} to print | |
| * @param headerFormat the page header, or {@code null} for none | |
| * @param footerFormat the page footer, or {@code null} for none | |
| * @return {@code TextComponentPrintable} to print {@code textComponent} | |
| */ | |
| public static Printable getPrintable(final JTextComponent textComponent, | |
| final MessageFormat headerFormat, | |
| final MessageFormat footerFormat) { | |
| if (textComponent instanceof JEditorPane | |
| && isFrameSetDocument(textComponent.getDocument())) { | |
| //for document with frames we create one printable per | |
| //frame and merge them with the CompoundPrintable. | |
| List<JEditorPane> frames = getFrames((JEditorPane) textComponent); | |
| List<CountingPrintable> printables = | |
| new ArrayList<CountingPrintable>(); | |
| for (JEditorPane frame : frames) { | |
| printables.add((CountingPrintable) | |
| getPrintable(frame, headerFormat, footerFormat)); | |
| } | |
| return new CompoundPrintable(printables); | |
| } else { | |
| return new TextComponentPrintable(textComponent, | |
| headerFormat, footerFormat); | |
| } | |
| } | |
| /** | |
| * Checks whether the document has frames. Only HTMLDocument might | |
| * have frames. | |
| * | |
| * @param document the {@code Document} to check | |
| * @return {@code true} if the {@code document} has frames | |
| */ | |
| private static boolean isFrameSetDocument(final Document document) { | |
| boolean ret = false; | |
| if (document instanceof HTMLDocument) { | |
| HTMLDocument htmlDocument = (HTMLDocument)document; | |
| if (htmlDocument.getIterator(HTML.Tag.FRAME).isValid()) { | |
| ret = true; | |
| } | |
| } | |
| return ret; | |
| } | |
| /** | |
| * Returns frames under the {@code editor}. | |
| * The frames are created if necessary. | |
| * | |
| * @param editor the {@link JEditorPane} to find the frames for | |
| * @return list of all frames | |
| */ | |
| private static List<JEditorPane> getFrames(final JEditorPane editor) { | |
| List<JEditorPane> list = new ArrayList<JEditorPane>(); | |
| getFrames(editor, list); | |
| if (list.size() == 0) { | |
| //the frames have not been created yet. | |
| //let's trigger the frames creation. | |
| createFrames(editor); | |
| getFrames(editor, list); | |
| } | |
| return list; | |
| } | |
| /** | |
| * Adds all {@code JEditorPanes} under {@code container} tagged by {@code | |
| * FrameEditorPaneTag} to the {@code list}. It adds only top | |
| * level {@code JEditorPanes}. For instance if there is a frame | |
| * inside the frame it will return the top frame only. | |
| * | |
| * @param container the container to find all frames under | |
| * @param list {@code List} to append the results too | |
| */ | |
| private static void getFrames(final Container container, List<JEditorPane> list) { | |
| for (Component c : container.getComponents()) { | |
| if (c instanceof FrameEditorPaneTag | |
| && c instanceof JEditorPane ) { //it should be always JEditorPane | |
| list.add((JEditorPane) c); | |
| } else { | |
| if (c instanceof Container) { | |
| getFrames((Container) c, list); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Triggers the frames creation for {@code JEditorPane} | |
| * | |
| * @param editor the {@code JEditorPane} to create frames for | |
| */ | |
| private static void createFrames(final JEditorPane editor) { | |
| Runnable doCreateFrames = | |
| new Runnable() { | |
| public void run() { | |
| final int WIDTH = 500; | |
| final int HEIGHT = 500; | |
| CellRendererPane rendererPane = new CellRendererPane(); | |
| rendererPane.add(editor); | |
| //the values do not matter | |
| //we only need to get frames created | |
| rendererPane.setSize(WIDTH, HEIGHT); | |
| } | |
| }; | |
| if (SwingUtilities.isEventDispatchThread()) { | |
| doCreateFrames.run(); | |
| } else { | |
| try { | |
| SwingUtilities.invokeAndWait(doCreateFrames); | |
| } catch (Exception e) { | |
| if (e instanceof RuntimeException) { | |
| throw (RuntimeException) e; | |
| } else { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Constructs {@code TextComponentPrintable} to print {@code JTextComponent} | |
| * {@code textComponent} with {@code headerFormat} and {@code footerFormat}. | |
| * | |
| * @param textComponent {@code JTextComponent} to print | |
| * @param headerFormat the page header or {@code null} for none | |
| * @param footerFormat the page footer or {@code null} for none | |
| */ | |
| private TextComponentPrintable(JTextComponent textComponent, | |
| MessageFormat headerFormat, | |
| MessageFormat footerFormat) { | |
| this.textComponentToPrint = textComponent; | |
| this.headerFormat = headerFormat; | |
| this.footerFormat = footerFormat; | |
| headerFont = textComponent.getFont().deriveFont(Font.BOLD, | |
| HEADER_FONT_SIZE); | |
| footerFont = textComponent.getFont().deriveFont(Font.PLAIN, | |
| FOOTER_FONT_SIZE); | |
| this.pagesMetrics = | |
| Collections.synchronizedList(new ArrayList<IntegerSegment>()); | |
| this.rowsMetrics = new ArrayList<IntegerSegment>(LIST_SIZE); | |
| this.printShell = createPrintShell(textComponent); | |
| } | |
| /** | |
| * creates a printShell. | |
| * It creates closest text component to {@code textComponent} | |
| * which uses {@code frc} from the {@code TextComponentPrintable} | |
| * for the {@code getFontMetrics} method. | |
| * | |
| * @param textComponent {@code JTextComponent} to create a | |
| * printShell for | |
| * @return the print shell | |
| */ | |
| private JTextComponent createPrintShell(final JTextComponent textComponent) { | |
| if (SwingUtilities.isEventDispatchThread()) { | |
| return createPrintShellOnEDT(textComponent); | |
| } else { | |
| FutureTask<JTextComponent> futureCreateShell = | |
| new FutureTask<JTextComponent>( | |
| new Callable<JTextComponent>() { | |
| public JTextComponent call() throws Exception { | |
| return createPrintShellOnEDT(textComponent); | |
| } | |
| }); | |
| SwingUtilities.invokeLater(futureCreateShell); | |
| try { | |
| return futureCreateShell.get(); | |
| } catch (InterruptedException e) { | |
| throw new RuntimeException(e); | |
| } catch (ExecutionException e) { | |
| Throwable cause = e.getCause(); | |
| if (cause instanceof Error) { | |
| throw (Error) cause; | |
| } | |
| if (cause instanceof RuntimeException) { | |
| throw (RuntimeException) cause; | |
| } | |
| throw new AssertionError(cause); | |
| } | |
| } | |
| } | |
| private JTextComponent createPrintShellOnEDT(final JTextComponent textComponent) { | |
| assert SwingUtilities.isEventDispatchThread(); | |
| JTextComponent ret = null; | |
| if (textComponent instanceof JPasswordField) { | |
| ret = | |
| new JPasswordField() { | |
| { | |
| setEchoChar(((JPasswordField) textComponent).getEchoChar()); | |
| setHorizontalAlignment( | |
| ((JTextField) textComponent).getHorizontalAlignment()); | |
| } | |
| @Override | |
| public FontMetrics getFontMetrics(Font font) { | |
| FontRenderContext frc = TextComponentPrintable.this.frc; | |
| return (frc == null) | |
| ? super.getFontMetrics(font) | |
| : FontDesignMetrics.getMetrics(font, frc); | |
| } | |
| }; | |
| } else if (textComponent instanceof JTextField) { | |
| ret = | |
| new JTextField() { | |
| { | |
| setHorizontalAlignment( | |
| ((JTextField) textComponent).getHorizontalAlignment()); | |
| } | |
| @Override | |
| public FontMetrics getFontMetrics(Font font) { | |
| FontRenderContext frc = TextComponentPrintable.this.frc; | |
| return (frc == null) | |
| ? super.getFontMetrics(font) | |
| : FontDesignMetrics.getMetrics(font, frc); | |
| } | |
| }; | |
| } else if (textComponent instanceof JTextArea) { | |
| ret = | |
| new JTextArea() { | |
| { | |
| JTextArea textArea = (JTextArea) textComponent; | |
| setLineWrap(textArea.getLineWrap()); | |
| setWrapStyleWord(textArea.getWrapStyleWord()); | |
| setTabSize(textArea.getTabSize()); | |
| } | |
| @Override | |
| public FontMetrics getFontMetrics(Font font) { | |
| FontRenderContext frc = TextComponentPrintable.this.frc; | |
| return (frc == null) | |
| ? super.getFontMetrics(font) | |
| : FontDesignMetrics.getMetrics(font, frc); | |
| } | |
| }; | |
| } else if (textComponent instanceof JTextPane) { | |
| ret = | |
| new JTextPane() { | |
| @Override | |
| public FontMetrics getFontMetrics(Font font) { | |
| FontRenderContext frc = TextComponentPrintable.this.frc; | |
| return (frc == null) | |
| ? super.getFontMetrics(font) | |
| : FontDesignMetrics.getMetrics(font, frc); | |
| } | |
| @Override | |
| public EditorKit getEditorKit() { | |
| if (getDocument() == textComponent.getDocument()) { | |
| return ((JTextPane) textComponent).getEditorKit(); | |
| } else { | |
| return super.getEditorKit(); | |
| } | |
| } | |
| }; | |
| } else if (textComponent instanceof JEditorPane) { | |
| ret = | |
| new JEditorPane() { | |
| @Override | |
| public FontMetrics getFontMetrics(Font font) { | |
| FontRenderContext frc = TextComponentPrintable.this.frc; | |
| return (frc == null) | |
| ? super.getFontMetrics(font) | |
| : FontDesignMetrics.getMetrics(font, frc); | |
| } | |
| @Override | |
| public EditorKit getEditorKit() { | |
| if (getDocument() == textComponent.getDocument()) { | |
| return ((JEditorPane) textComponent).getEditorKit(); | |
| } else { | |
| return super.getEditorKit(); | |
| } | |
| } | |
| }; | |
| } | |
| //want to occupy the whole width and height by text | |
| ret.setBorder(null); | |
| //set properties from the component to print | |
| ret.setOpaque(textComponent.isOpaque()); | |
| ret.setEditable(textComponent.isEditable()); | |
| ret.setEnabled(textComponent.isEnabled()); | |
| ret.setFont(textComponent.getFont()); | |
| ret.setBackground(textComponent.getBackground()); | |
| ret.setForeground(textComponent.getForeground()); | |
| ret.setComponentOrientation( | |
| textComponent.getComponentOrientation()); | |
| if (ret instanceof JEditorPane) { | |
| ret.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, | |
| textComponent.getClientProperty( | |
| JEditorPane.HONOR_DISPLAY_PROPERTIES)); | |
| ret.putClientProperty(JEditorPane.W3C_LENGTH_UNITS, | |
| textComponent.getClientProperty(JEditorPane.W3C_LENGTH_UNITS)); | |
| ret.putClientProperty("charset", | |
| textComponent.getClientProperty("charset")); | |
| } | |
| ret.setDocument(textComponent.getDocument()); | |
| return ret; | |
| } | |
| /** | |
| * Returns the number of pages in this printable. | |
| * <p> | |
| * This number is defined only after {@code print} returns NO_SUCH_PAGE. | |
| * | |
| * @return the number of pages. | |
| */ | |
| public int getNumberOfPages() { | |
| return pagesMetrics.size(); | |
| } | |
| /** | |
| * See Printable.print for the API description. | |
| * | |
| * There are two parts in the implementation. | |
| * First part (print) is to be called on the printing thread. | |
| * Second part (printOnEDT) is to be called on the EDT only. | |
| * | |
| * print triggers printOnEDT | |
| */ | |
| public int print(final Graphics graphics, | |
| final PageFormat pf, | |
| final int pageIndex) throws PrinterException { | |
| if (!isLayouted) { | |
| if (graphics instanceof Graphics2D) { | |
| frc = ((Graphics2D)graphics).getFontRenderContext(); | |
| } | |
| layout((int)Math.floor(pf.getImageableWidth())); | |
| calculateRowsMetrics(); | |
| } | |
| int ret; | |
| if (!SwingUtilities.isEventDispatchThread()) { | |
| Callable<Integer> doPrintOnEDT = new Callable<Integer>() { | |
| public Integer call() throws Exception { | |
| return printOnEDT(graphics, pf, pageIndex); | |
| } | |
| }; | |
| FutureTask<Integer> futurePrintOnEDT = | |
| new FutureTask<Integer>(doPrintOnEDT); | |
| SwingUtilities.invokeLater(futurePrintOnEDT); | |
| try { | |
| ret = futurePrintOnEDT.get(); | |
| } catch (InterruptedException e) { | |
| throw new RuntimeException(e); | |
| } catch (ExecutionException e) { | |
| Throwable cause = e.getCause(); | |
| if (cause instanceof PrinterException) { | |
| throw (PrinterException)cause; | |
| } else if (cause instanceof RuntimeException) { | |
| throw (RuntimeException) cause; | |
| } else if (cause instanceof Error) { | |
| throw (Error) cause; | |
| } else { | |
| throw new RuntimeException(cause); | |
| } | |
| } | |
| } else { | |
| ret = printOnEDT(graphics, pf, pageIndex); | |
| } | |
| return ret; | |
| } | |
| /** | |
| * The EDT part of the print method. | |
| * | |
| * This method is to be called on the EDT only. Layout should be done before | |
| * calling this method. | |
| */ | |
| private int printOnEDT(final Graphics graphics, | |
| final PageFormat pf, | |
| final int pageIndex) throws PrinterException { | |
| assert SwingUtilities.isEventDispatchThread(); | |
| Border border = BorderFactory.createEmptyBorder(); | |
| //handle header and footer | |
| if (headerFormat != null || footerFormat != null) { | |
| //Printable page enumeration is 0 base. We need 1 based. | |
| Object[] formatArg = new Object[]{Integer.valueOf(pageIndex + 1)}; | |
| if (headerFormat != null) { | |
| border = new TitledBorder(border, | |
| headerFormat.format(formatArg), | |
| TitledBorder.CENTER, TitledBorder.ABOVE_TOP, | |
| headerFont, printShell.getForeground()); | |
| } | |
| if (footerFormat != null) { | |
| border = new TitledBorder(border, | |
| footerFormat.format(formatArg), | |
| TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM, | |
| footerFont, printShell.getForeground()); | |
| } | |
| } | |
| Insets borderInsets = border.getBorderInsets(printShell); | |
| updatePagesMetrics(pageIndex, | |
| (int)Math.floor(pf.getImageableHeight()) - borderInsets.top | |
| - borderInsets.bottom); | |
| if (pagesMetrics.size() <= pageIndex) { | |
| return NO_SUCH_PAGE; | |
| } | |
| Graphics2D g2d = (Graphics2D)graphics.create(); | |
| g2d.translate(pf.getImageableX(), pf.getImageableY()); | |
| border.paintBorder(printShell, g2d, 0, 0, | |
| (int)Math.floor(pf.getImageableWidth()), | |
| (int)Math.floor(pf.getImageableHeight())); | |
| g2d.translate(0, borderInsets.top); | |
| //want to clip only vertically | |
| Rectangle clip = new Rectangle(0, 0, | |
| (int) pf.getWidth(), | |
| pagesMetrics.get(pageIndex).end | |
| - pagesMetrics.get(pageIndex).start + 1); | |
| g2d.clip(clip); | |
| int xStart = 0; | |
| if (ComponentOrientation.RIGHT_TO_LEFT == | |
| printShell.getComponentOrientation()) { | |
| xStart = (int) pf.getImageableWidth() - printShell.getWidth(); | |
| } | |
| g2d.translate(xStart, - pagesMetrics.get(pageIndex).start); | |
| printShell.print(g2d); | |
| g2d.dispose(); | |
| return Printable.PAGE_EXISTS; | |
| } | |
| private boolean needReadLock = false; | |
| /** | |
| * Tries to release document's readlock | |
| * | |
| * Note: Not to be called on the EDT. | |
| */ | |
| private void releaseReadLock() { | |
| assert ! SwingUtilities.isEventDispatchThread(); | |
| Document document = textComponentToPrint.getDocument(); | |
| if (document instanceof AbstractDocument) { | |
| try { | |
| ((AbstractDocument) document).readUnlock(); | |
| needReadLock = true; | |
| } catch (Error ignore) { | |
| // readUnlock() might throw StateInvariantError | |
| } | |
| } | |
| } | |
| /** | |
| * Tries to acquire document's readLock if it was released | |
| * in releaseReadLock() method. | |
| * | |
| * Note: Not to be called on the EDT. | |
| */ | |
| private void acquireReadLock() { | |
| assert ! SwingUtilities.isEventDispatchThread(); | |
| if (needReadLock) { | |
| try { | |
| /* | |
| * wait until all the EDT events are processed | |
| * some of the document changes are asynchronous | |
| * we need to make sure we get the lock after those changes | |
| */ | |
| SwingUtilities.invokeAndWait( | |
| new Runnable() { | |
| public void run() { | |
| } | |
| }); | |
| } catch (InterruptedException | InvocationTargetException ignore) { | |
| } | |
| Document document = textComponentToPrint.getDocument(); | |
| ((AbstractDocument) document).readLock(); | |
| needReadLock = false; | |
| } | |
| } | |
| /** | |
| * Prepares {@code printShell} for printing. | |
| * | |
| * Sets properties from the component to print. | |
| * Sets width and FontRenderContext. | |
| * | |
| * Triggers Views creation for the printShell. | |
| * | |
| * There are two parts in the implementation. | |
| * First part (layout) is to be called on the printing thread. | |
| * Second part (layoutOnEDT) is to be called on the EDT only. | |
| * | |
| * {@code layout} triggers {@code layoutOnEDT}. | |
| * | |
| * @param width width to layout the text for | |
| */ | |
| private void layout(final int width) { | |
| if (!SwingUtilities.isEventDispatchThread()) { | |
| Callable<Object> doLayoutOnEDT = new Callable<Object>() { | |
| public Object call() throws Exception { | |
| layoutOnEDT(width); | |
| return null; | |
| } | |
| }; | |
| FutureTask<Object> futureLayoutOnEDT = new FutureTask<Object>( | |
| doLayoutOnEDT); | |
| /* | |
| * We need to release document's readlock while printShell is | |
| * initializing | |
| */ | |
| releaseReadLock(); | |
| SwingUtilities.invokeLater(futureLayoutOnEDT); | |
| try { | |
| futureLayoutOnEDT.get(); | |
| } catch (InterruptedException e) { | |
| throw new RuntimeException(e); | |
| } catch (ExecutionException e) { | |
| Throwable cause = e.getCause(); | |
| if (cause instanceof RuntimeException) { | |
| throw (RuntimeException) cause; | |
| } else if (cause instanceof Error) { | |
| throw (Error) cause; | |
| } else { | |
| throw new RuntimeException(cause); | |
| } | |
| } finally { | |
| acquireReadLock(); | |
| } | |
| } else { | |
| layoutOnEDT(width); | |
| } | |
| isLayouted = true; | |
| } | |
| /** | |
| * The EDT part of layout method. | |
| * | |
| * This method is to be called on the EDT only. | |
| */ | |
| private void layoutOnEDT(final int width) { | |
| assert SwingUtilities.isEventDispatchThread(); | |
| //need to have big value but smaller than MAX_VALUE otherwise | |
| //printing goes south due to overflow somewhere | |
| final int HUGE_INTEGER = Integer.MAX_VALUE - 1000; | |
| CellRendererPane rendererPane = new CellRendererPane(); | |
| //need to use JViewport since text is layouted to the viewPort width | |
| //otherwise it will be layouted to the maximum text width | |
| JViewport viewport = new JViewport(); | |
| viewport.setBorder(null); | |
| Dimension size = new Dimension(width, HUGE_INTEGER); | |
| //JTextField is a special case | |
| //it layouts text in the middle by Y | |
| if (printShell instanceof JTextField) { | |
| size = | |
| new Dimension(size.width, printShell.getPreferredSize().height); | |
| } | |
| printShell.setSize(size); | |
| viewport.setComponentOrientation(printShell.getComponentOrientation()); | |
| viewport.setSize(size); | |
| viewport.add(printShell); | |
| rendererPane.add(viewport); | |
| } | |
| /** | |
| * Calculates pageMetrics for the pages up to the {@code pageIndex} using | |
| * {@code rowsMetrics}. | |
| * Metrics are stored in the {@code pagesMetrics}. | |
| * | |
| * @param pageIndex the page to update the metrics for | |
| * @param pageHeight the page height | |
| */ | |
| private void updatePagesMetrics(final int pageIndex, final int pageHeight) { | |
| while (pageIndex >= pagesMetrics.size() && !rowsMetrics.isEmpty()) { | |
| // add one page to the pageMetrics | |
| int lastPage = pagesMetrics.size() - 1; | |
| int pageStart = (lastPage >= 0) | |
| ? pagesMetrics.get(lastPage).end + 1 | |
| : 0; | |
| int rowIndex; | |
| for (rowIndex = 0; | |
| rowIndex < rowsMetrics.size() | |
| && (rowsMetrics.get(rowIndex).end - pageStart + 1) | |
| <= pageHeight; | |
| rowIndex++) { | |
| } | |
| if (rowIndex == 0) { | |
| // can not fit a single row | |
| // need to split | |
| pagesMetrics.add( | |
| new IntegerSegment(pageStart, pageStart + pageHeight - 1)); | |
| } else { | |
| rowIndex--; | |
| pagesMetrics.add(new IntegerSegment(pageStart, | |
| rowsMetrics.get(rowIndex).end)); | |
| for (int i = 0; i <= rowIndex; i++) { | |
| rowsMetrics.remove(0); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Calculates rowsMetrics for the document. The result is stored | |
| * in the {@code rowsMetrics}. | |
| * | |
| * Two steps process. | |
| * First step is to find yStart and yEnd for the every document position. | |
| * Second step is to merge all intersected segments ( [yStart, yEnd] ). | |
| */ | |
| @SuppressWarnings("deprecation") | |
| private void calculateRowsMetrics() { | |
| final int documentLength = printShell.getDocument().getLength(); | |
| List<IntegerSegment> documentMetrics = new ArrayList<IntegerSegment>(LIST_SIZE); | |
| Rectangle rect; | |
| for (int i = 0, previousY = -1, previousHeight = -1; i < documentLength; | |
| i++) { | |
| try { | |
| rect = printShell.modelToView(i); | |
| if (rect != null) { | |
| int y = (int) rect.getY(); | |
| int height = (int) rect.getHeight(); | |
| if (height != 0 | |
| && (y != previousY || height != previousHeight)) { | |
| /* | |
| * We do not store the same value as previous. | |
| * In our documents, it is common for consecutive | |
| * positions to have the same modelToView y and height. | |
| */ | |
| previousY = y; | |
| previousHeight = height; | |
| documentMetrics.add(new IntegerSegment(y, y + height - 1)); | |
| } | |
| } | |
| } catch (BadLocationException e) { | |
| assert false; | |
| } | |
| } | |
| /* | |
| * Merge all intersected segments. | |
| */ | |
| Collections.sort(documentMetrics); | |
| int yStart = Integer.MIN_VALUE; | |
| int yEnd = Integer.MIN_VALUE; | |
| for (IntegerSegment segment : documentMetrics) { | |
| if (yEnd < segment.start) { | |
| if (yEnd != Integer.MIN_VALUE) { | |
| rowsMetrics.add(new IntegerSegment(yStart, yEnd)); | |
| } | |
| yStart = segment.start; | |
| yEnd = segment.end; | |
| } else { | |
| yEnd = segment.end; | |
| } | |
| } | |
| if (yEnd != Integer.MIN_VALUE) { | |
| rowsMetrics.add(new IntegerSegment(yStart, yEnd)); | |
| } | |
| } | |
| /** | |
| * Class to represent segment of integers. | |
| * we do not call it Segment to avoid confusion with | |
| * javax.swing.text.Segment | |
| */ | |
| private static class IntegerSegment implements Comparable<IntegerSegment> { | |
| final int start; | |
| final int end; | |
| IntegerSegment(int start, int end) { | |
| this.start = start; | |
| this.end = end; | |
| } | |
| public int compareTo(IntegerSegment object) { | |
| int startsDelta = start - object.start; | |
| return (startsDelta != 0) ? startsDelta : end - object.end; | |
| } | |
| @Override | |
| public boolean equals(Object obj) { | |
| if (obj instanceof IntegerSegment) { | |
| return compareTo((IntegerSegment) obj) == 0; | |
| } else { | |
| return false; | |
| } | |
| } | |
| @Override | |
| public int hashCode() { | |
| // from the "Effective Java: Programming Language Guide" | |
| int result = 17; | |
| result = 37 * result + start; | |
| result = 37 * result + end; | |
| return result; | |
| } | |
| @Override | |
| public String toString() { | |
| return "IntegerSegment [" + start + ", " + end + "]"; | |
| } | |
| } | |
| } |