Path: blob/master/src/java.desktop/share/classes/sun/swing/text/TextComponentPrintable.java
41155 views
/*1* Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/24package sun.swing.text;2526import java.awt.ComponentOrientation;27import java.awt.Dimension;28import java.awt.Font;29import java.awt.FontMetrics;30import java.awt.Graphics;31import java.awt.Graphics2D;32import java.awt.Insets;33import java.awt.Rectangle;34import java.awt.Component;35import java.awt.Container;36import java.awt.font.FontRenderContext;37import java.awt.print.PageFormat;38import java.awt.print.Printable;39import java.awt.print.PrinterException;40import java.text.MessageFormat;41import java.util.ArrayList;42import java.util.Collections;43import java.util.List;44import java.util.concurrent.Callable;45import java.util.concurrent.ExecutionException;46import java.util.concurrent.FutureTask;47import java.util.concurrent.atomic.AtomicReference;4849import javax.swing.*;50import javax.swing.border.Border;51import javax.swing.border.TitledBorder;52import javax.swing.text.BadLocationException;53import javax.swing.text.JTextComponent;54import javax.swing.text.Document;55import javax.swing.text.EditorKit;56import javax.swing.text.AbstractDocument;57import javax.swing.text.html.HTMLDocument;58import javax.swing.text.html.HTML;5960import sun.font.FontDesignMetrics;6162import sun.swing.text.html.FrameEditorPaneTag;6364/**65* An implementation of {@code Printable} to print {@code JTextComponent} with66* the header and footer.67*68* <h1>69* WARNING: this class is to be used in70* javax.swing.text.JTextComponent only.71* </h1>72*73* <p>74* The implementation creates a new {@code JTextComponent} ({@code printShell})75* to print the content using the {@code Document}, {@code EditorKit} and76* rendering-affecting properties from the original {@code JTextComponent}.77*78* <p>79* {@code printShell} is laid out on the first {@code print} invocation.80*81* <p>82* This class can be used on any thread. Part of the implementation is executed83* on the EDT though.84*85* @author Igor Kushnirskiy86*87* @since 1.688*/89public class TextComponentPrintable implements CountingPrintable {909192private static final int LIST_SIZE = 1000;9394private boolean isLayouted = false;9596/*97* The text component to print.98*/99private final JTextComponent textComponentToPrint;100101/*102* The FontRenderContext to layout and print with103*/104private final AtomicReference<FontRenderContext> frc =105new AtomicReference<FontRenderContext>(null);106107/**108* Special text component used to print to the printer.109*/110private final JTextComponent printShell;111112private final MessageFormat headerFormat;113private final MessageFormat footerFormat;114115private static final float HEADER_FONT_SIZE = 18.0f;116private static final float FOOTER_FONT_SIZE = 12.0f;117118private final Font headerFont;119private final Font footerFont;120121/**122* stores metrics for the unhandled rows. The only metrics we need are123* yStart and yEnd when row is handled by updatePagesMetrics it is removed124* from the list. Thus the head of the list is the fist row to handle.125*126* sorted127*/128private final List<IntegerSegment> rowsMetrics;129130/**131* thread-safe list for storing pages metrics. The only metrics we need are132* yStart and yEnd.133* It has to be thread-safe since metrics are calculated on134* the printing thread and accessed on the EDT thread.135*136* sorted137*/138private final List<IntegerSegment> pagesMetrics;139140/**141* Returns {@code TextComponentPrintable} to print {@code textComponent}.142*143* @param textComponent {@code JTextComponent} to print144* @param headerFormat the page header, or {@code null} for none145* @param footerFormat the page footer, or {@code null} for none146* @return {@code TextComponentPrintable} to print {@code textComponent}147*/148public static Printable getPrintable(final JTextComponent textComponent,149final MessageFormat headerFormat,150final MessageFormat footerFormat) {151152if (textComponent instanceof JEditorPane153&& isFrameSetDocument(textComponent.getDocument())) {154//for document with frames we create one printable per155//frame and merge them with the CompoundPrintable.156List<JEditorPane> frames = getFrames((JEditorPane) textComponent);157List<CountingPrintable> printables =158new ArrayList<CountingPrintable>();159for (JEditorPane frame : frames) {160printables.add((CountingPrintable)161getPrintable(frame, headerFormat, footerFormat));162}163return new CompoundPrintable(printables);164} else {165return new TextComponentPrintable(textComponent,166headerFormat, footerFormat);167}168}169170/**171* Checks whether the document has frames. Only HTMLDocument might172* have frames.173*174* @param document the {@code Document} to check175* @return {@code true} if the {@code document} has frames176*/177private static boolean isFrameSetDocument(final Document document) {178boolean ret = false;179if (document instanceof HTMLDocument) {180HTMLDocument htmlDocument = (HTMLDocument)document;181if (htmlDocument.getIterator(HTML.Tag.FRAME).isValid()) {182ret = true;183}184}185return ret;186}187188189/**190* Returns frames under the {@code editor}.191* The frames are created if necessary.192*193* @param editor the {@JEditorPane} to find the frames for194* @return list of all frames195*/196private static List<JEditorPane> getFrames(final JEditorPane editor) {197List<JEditorPane> list = new ArrayList<JEditorPane>();198getFrames(editor, list);199if (list.size() == 0) {200//the frames have not been created yet.201//let's trigger the frames creation.202createFrames(editor);203getFrames(editor, list);204}205return list;206}207208/**209* Adds all {@code JEditorPanes} under {@code container} tagged by {@code210* FrameEditorPaneTag} to the {@code list}. It adds only top211* level {@code JEditorPanes}. For instance if there is a frame212* inside the frame it will return the top frame only.213*214* @param container the container to find all frames under215* @param list {@code List} to append the results too216*/217private static void getFrames(final Container container, List<JEditorPane> list) {218for (Component c : container.getComponents()) {219if (c instanceof FrameEditorPaneTag220&& c instanceof JEditorPane ) { //it should be always JEditorPane221list.add((JEditorPane) c);222} else {223if (c instanceof Container) {224getFrames((Container) c, list);225}226}227}228}229230/**231* Triggers the frames creation for {@code JEditorPane}232*233* @param editor the {@code JEditorPane} to create frames for234*/235private static void createFrames(final JEditorPane editor) {236Runnable doCreateFrames =237new Runnable() {238public void run() {239final int WIDTH = 500;240final int HEIGHT = 500;241CellRendererPane rendererPane = new CellRendererPane();242rendererPane.add(editor);243//the values do not matter244//we only need to get frames created245rendererPane.setSize(WIDTH, HEIGHT);246};247};248if (SwingUtilities.isEventDispatchThread()) {249doCreateFrames.run();250} else {251try {252SwingUtilities.invokeAndWait(doCreateFrames);253} catch (Exception e) {254if (e instanceof RuntimeException) {255throw (RuntimeException) e;256} else {257throw new RuntimeException(e);258}259}260}261}262263/**264* Constructs {@code TextComponentPrintable} to print {@code JTextComponent}265* {@code textComponent} with {@code headerFormat} and {@code footerFormat}.266*267* @param textComponent {@code JTextComponent} to print268* @param headerFormat the page header or {@code null} for none269* @param footerFormat the page footer or {@code null} for none270*/271private TextComponentPrintable(JTextComponent textComponent,272MessageFormat headerFormat,273MessageFormat footerFormat) {274this.textComponentToPrint = textComponent;275this.headerFormat = headerFormat;276this.footerFormat = footerFormat;277headerFont = textComponent.getFont().deriveFont(Font.BOLD,278HEADER_FONT_SIZE);279footerFont = textComponent.getFont().deriveFont(Font.PLAIN,280FOOTER_FONT_SIZE);281this.pagesMetrics =282Collections.synchronizedList(new ArrayList<IntegerSegment>());283this.rowsMetrics = new ArrayList<IntegerSegment>(LIST_SIZE);284this.printShell = createPrintShell(textComponent);285}286287288/**289* creates a printShell.290* It creates closest text component to {@code textComponent}291* which uses {@code frc} from the {@code TextComponentPrintable}292* for the {@code getFontMetrics} method.293*294* @param textComponent {@code JTextComponent} to create a295* printShell for296* @return the print shell297*/298private JTextComponent createPrintShell(final JTextComponent textComponent) {299if (SwingUtilities.isEventDispatchThread()) {300return createPrintShellOnEDT(textComponent);301} else {302FutureTask<JTextComponent> futureCreateShell =303new FutureTask<JTextComponent>(304new Callable<JTextComponent>() {305public JTextComponent call() throws Exception {306return createPrintShellOnEDT(textComponent);307}308});309SwingUtilities.invokeLater(futureCreateShell);310try {311return futureCreateShell.get();312} catch (InterruptedException e) {313throw new RuntimeException(e);314} catch (ExecutionException e) {315Throwable cause = e.getCause();316if (cause instanceof Error) {317throw (Error) cause;318}319if (cause instanceof RuntimeException) {320throw (RuntimeException) cause;321}322throw new AssertionError(cause);323}324}325}326@SuppressWarnings("serial") // anonymous class inside327private JTextComponent createPrintShellOnEDT(final JTextComponent textComponent) {328assert SwingUtilities.isEventDispatchThread();329330JTextComponent ret = null;331if (textComponent instanceof JPasswordField) {332ret =333new JPasswordField() {334{335setEchoChar(((JPasswordField) textComponent).getEchoChar());336setHorizontalAlignment(337((JTextField) textComponent).getHorizontalAlignment());338}339@Override340public FontMetrics getFontMetrics(Font font) {341return (frc.get() == null)342? super.getFontMetrics(font)343: FontDesignMetrics.getMetrics(font, frc.get());344}345};346} else if (textComponent instanceof JTextField) {347ret =348new JTextField() {349{350setHorizontalAlignment(351((JTextField) textComponent).getHorizontalAlignment());352}353@Override354public FontMetrics getFontMetrics(Font font) {355return (frc.get() == null)356? super.getFontMetrics(font)357: FontDesignMetrics.getMetrics(font, frc.get());358}359};360} else if (textComponent instanceof JTextArea) {361ret =362new JTextArea() {363{364JTextArea textArea = (JTextArea) textComponent;365setLineWrap(textArea.getLineWrap());366setWrapStyleWord(textArea.getWrapStyleWord());367setTabSize(textArea.getTabSize());368}369@Override370public FontMetrics getFontMetrics(Font font) {371return (frc.get() == null)372? super.getFontMetrics(font)373: FontDesignMetrics.getMetrics(font, frc.get());374}375};376} else if (textComponent instanceof JTextPane) {377ret =378new JTextPane() {379@Override380public FontMetrics getFontMetrics(Font font) {381return (frc.get() == null)382? super.getFontMetrics(font)383: FontDesignMetrics.getMetrics(font, frc.get());384}385@Override386public EditorKit getEditorKit() {387if (getDocument() == textComponent.getDocument()) {388return ((JTextPane) textComponent).getEditorKit();389} else {390return super.getEditorKit();391}392}393};394} else if (textComponent instanceof JEditorPane) {395ret =396new JEditorPane() {397@Override398public FontMetrics getFontMetrics(Font font) {399return (frc.get() == null)400? super.getFontMetrics(font)401: FontDesignMetrics.getMetrics(font, frc.get());402}403@Override404public EditorKit getEditorKit() {405if (getDocument() == textComponent.getDocument()) {406return ((JEditorPane) textComponent).getEditorKit();407} else {408return super.getEditorKit();409}410}411};412}413//want to occupy the whole width and height by text414ret.setBorder(null);415416//set properties from the component to print417ret.setOpaque(textComponent.isOpaque());418ret.setEditable(textComponent.isEditable());419ret.setEnabled(textComponent.isEnabled());420ret.setFont(textComponent.getFont());421ret.setBackground(textComponent.getBackground());422ret.setForeground(textComponent.getForeground());423ret.setComponentOrientation(424textComponent.getComponentOrientation());425426if (ret instanceof JEditorPane) {427ret.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES,428textComponent.getClientProperty(429JEditorPane.HONOR_DISPLAY_PROPERTIES));430ret.putClientProperty(JEditorPane.W3C_LENGTH_UNITS,431textComponent.getClientProperty(JEditorPane.W3C_LENGTH_UNITS));432ret.putClientProperty("charset",433textComponent.getClientProperty("charset"));434}435ret.setDocument(textComponent.getDocument());436return ret;437}438439440441442/**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*/449public int getNumberOfPages() {450return pagesMetrics.size();451}452453/**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 printOnEDT461*/462public int print(final Graphics graphics,463final PageFormat pf,464final int pageIndex) throws PrinterException {465if (!isLayouted) {466if (graphics instanceof Graphics2D) {467frc.set(((Graphics2D)graphics).getFontRenderContext());468}469layout((int)Math.floor(pf.getImageableWidth()));470calculateRowsMetrics();471}472int ret;473if (!SwingUtilities.isEventDispatchThread()) {474Callable<Integer> doPrintOnEDT = new Callable<Integer>() {475public Integer call() throws Exception {476return printOnEDT(graphics, pf, pageIndex);477}478};479FutureTask<Integer> futurePrintOnEDT =480new FutureTask<Integer>(doPrintOnEDT);481SwingUtilities.invokeLater(futurePrintOnEDT);482try {483ret = futurePrintOnEDT.get();484} catch (InterruptedException e) {485throw new RuntimeException(e);486} catch (ExecutionException e) {487Throwable cause = e.getCause();488if (cause instanceof PrinterException) {489throw (PrinterException)cause;490} else if (cause instanceof RuntimeException) {491throw (RuntimeException) cause;492} else if (cause instanceof Error) {493throw (Error) cause;494} else {495throw new RuntimeException(cause);496}497}498} else {499ret = printOnEDT(graphics, pf, pageIndex);500}501return ret;502}503504505/**506* The EDT part of the print method.507*508* This method is to be called on the EDT only. Layout should be done before509* calling this method.510*/511private int printOnEDT(final Graphics graphics,512final PageFormat pf,513final int pageIndex) throws PrinterException {514assert SwingUtilities.isEventDispatchThread();515Border border = BorderFactory.createEmptyBorder();516//handle header and footer517if (headerFormat != null || footerFormat != null) {518//Printable page enumeration is 0 base. We need 1 based.519Object[] formatArg = new Object[]{Integer.valueOf(pageIndex + 1)};520if (headerFormat != null) {521border = new TitledBorder(border,522headerFormat.format(formatArg),523TitledBorder.CENTER, TitledBorder.ABOVE_TOP,524headerFont, printShell.getForeground());525}526if (footerFormat != null) {527border = new TitledBorder(border,528footerFormat.format(formatArg),529TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM,530footerFont, printShell.getForeground());531}532}533Insets borderInsets = border.getBorderInsets(printShell);534updatePagesMetrics(pageIndex,535(int)Math.floor(pf.getImageableHeight()) - borderInsets.top536- borderInsets.bottom);537538if (pagesMetrics.size() <= pageIndex) {539return NO_SUCH_PAGE;540}541542Graphics2D g2d = (Graphics2D)graphics.create();543544g2d.translate(pf.getImageableX(), pf.getImageableY());545border.paintBorder(printShell, g2d, 0, 0,546(int)Math.floor(pf.getImageableWidth()),547(int)Math.floor(pf.getImageableHeight()));548549g2d.translate(0, borderInsets.top);550//want to clip only vertically551Rectangle clip = new Rectangle(0, 0,552(int) pf.getWidth(),553pagesMetrics.get(pageIndex).end554- pagesMetrics.get(pageIndex).start + 1);555556g2d.clip(clip);557int xStart = 0;558if (ComponentOrientation.RIGHT_TO_LEFT ==559printShell.getComponentOrientation()) {560xStart = (int) pf.getImageableWidth() - printShell.getWidth();561}562g2d.translate(xStart, - pagesMetrics.get(pageIndex).start);563printShell.print(g2d);564565g2d.dispose();566567return Printable.PAGE_EXISTS;568}569570571private boolean needReadLock = false;572573/**574* Tries to release document's readlock575*576* Note: Not to be called on the EDT.577*/578private void releaseReadLock() {579assert ! SwingUtilities.isEventDispatchThread();580Document document = textComponentToPrint.getDocument();581if (document instanceof AbstractDocument) {582try {583((AbstractDocument) document).readUnlock();584needReadLock = true;585} catch (Error ignore) {586// readUnlock() might throw StateInvariantError587}588}589}590591592/**593* Tries to acquire document's readLock if it was released594* in releaseReadLock() method.595*596* Note: Not to be called on the EDT.597*/598private void acquireReadLock() {599assert ! SwingUtilities.isEventDispatchThread();600if (needReadLock) {601try {602/*603* wait until all the EDT events are processed604* some of the document changes are asynchronous605* we need to make sure we get the lock after those changes606*/607SwingUtilities.invokeAndWait(608new Runnable() {609public void run() {610}611});612} catch (InterruptedException ignore) {613} catch (java.lang.reflect.InvocationTargetException ignore) {614}615Document document = textComponentToPrint.getDocument();616((AbstractDocument) document).readLock();617needReadLock = false;618}619}620621/**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 for636*/637private void layout(final int width) {638if (!SwingUtilities.isEventDispatchThread()) {639Callable<Object> doLayoutOnEDT = new Callable<Object>() {640public Object call() throws Exception {641layoutOnEDT(width);642return null;643}644};645FutureTask<Object> futureLayoutOnEDT = new FutureTask<Object>(646doLayoutOnEDT);647648/*649* We need to release document's readlock while printShell is650* initializing651*/652releaseReadLock();653SwingUtilities.invokeLater(futureLayoutOnEDT);654try {655futureLayoutOnEDT.get();656} catch (InterruptedException e) {657throw new RuntimeException(e);658} catch (ExecutionException e) {659Throwable cause = e.getCause();660if (cause instanceof RuntimeException) {661throw (RuntimeException) cause;662} else if (cause instanceof Error) {663throw (Error) cause;664} else {665throw new RuntimeException(cause);666}667} finally {668acquireReadLock();669}670} else {671layoutOnEDT(width);672}673674isLayouted = true;675}676677/**678* The EDT part of layout method.679*680* This method is to be called on the EDT only.681*/682private void layoutOnEDT(final int width) {683assert SwingUtilities.isEventDispatchThread();684//need to have big value but smaller than MAX_VALUE otherwise685//printing goes south due to overflow somewhere686final int HUGE_INTEGER = Integer.MAX_VALUE - 1000;687688CellRendererPane rendererPane = new CellRendererPane();689690//need to use JViewport since text is layouted to the viewPort width691//otherwise it will be layouted to the maximum text width692JViewport viewport = new JViewport();693viewport.setBorder(null);694Dimension size = new Dimension(width, HUGE_INTEGER);695696//JTextField is a special case697//it layouts text in the middle by Y698if (printShell instanceof JTextField) {699size =700new Dimension(size.width, printShell.getPreferredSize().height);701}702printShell.setSize(size);703viewport.setComponentOrientation(printShell.getComponentOrientation());704viewport.setSize(size);705viewport.add(printShell);706rendererPane.add(viewport);707}708709/**710* Calculates pageMetrics for the pages up to the {@code pageIndex} using711* {@code rowsMetrics}.712* Metrics are stored in the {@code pagesMetrics}.713*714* @param pageIndex the page to update the metrics for715* @param pageHeight the page height716*/717private void updatePagesMetrics(final int pageIndex, final int pageHeight) {718while (pageIndex >= pagesMetrics.size() && !rowsMetrics.isEmpty()) {719// add one page to the pageMetrics720int lastPage = pagesMetrics.size() - 1;721int pageStart = (lastPage >= 0)722? pagesMetrics.get(lastPage).end + 1723: 0;724int rowIndex;725for (rowIndex = 0;726rowIndex < rowsMetrics.size()727&& (rowsMetrics.get(rowIndex).end - pageStart + 1)728<= pageHeight;729rowIndex++) {730}731if (rowIndex == 0) {732// can not fit a single row733// need to split734pagesMetrics.add(735new IntegerSegment(pageStart, pageStart + pageHeight - 1));736} else {737rowIndex--;738pagesMetrics.add(new IntegerSegment(pageStart,739rowsMetrics.get(rowIndex).end));740for (int i = 0; i <= rowIndex; i++) {741rowsMetrics.remove(0);742}743}744}745}746747/**748* Calculates rowsMetrics for the document. The result is stored749* 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@SuppressWarnings("deprecation")756private void calculateRowsMetrics() {757final int documentLength = printShell.getDocument().getLength();758List<IntegerSegment> documentMetrics = new ArrayList<IntegerSegment>(LIST_SIZE);759Rectangle rect;760for (int i = 0, previousY = -1, previousHeight = -1; i < documentLength;761i++) {762try {763rect = printShell.modelToView(i);764if (rect != null) {765int y = (int) rect.getY();766int height = (int) rect.getHeight();767if (height != 0768&& (y != previousY || height != previousHeight)) {769/*770* we do not store the same value as previous. in our771* documents it is often for consequent positons to have772* the same modelToView y and height.773*/774previousY = y;775previousHeight = height;776documentMetrics.add(new IntegerSegment(y, y + height - 1));777}778}779} catch (BadLocationException e) {780assert false;781}782}783/*784* Merge all intersected segments.785*/786Collections.sort(documentMetrics);787int yStart = Integer.MIN_VALUE;788int yEnd = Integer.MIN_VALUE;789for (IntegerSegment segment : documentMetrics) {790if (yEnd < segment.start) {791if (yEnd != Integer.MIN_VALUE) {792rowsMetrics.add(new IntegerSegment(yStart, yEnd));793}794yStart = segment.start;795yEnd = segment.end;796} else {797yEnd = segment.end;798}799}800if (yEnd != Integer.MIN_VALUE) {801rowsMetrics.add(new IntegerSegment(yStart, yEnd));802}803}804805/**806* Class to represent segment of integers.807* we do not call it Segment to avoid confusion with808* javax.swing.text.Segment809*/810private static class IntegerSegment implements Comparable<IntegerSegment> {811final int start;812final int end;813814IntegerSegment(int start, int end) {815this.start = start;816this.end = end;817}818819public int compareTo(IntegerSegment object) {820int startsDelta = start - object.start;821return (startsDelta != 0) ? startsDelta : end - object.end;822}823824@Override825public boolean equals(Object obj) {826if (obj instanceof IntegerSegment) {827return compareTo((IntegerSegment) obj) == 0;828} else {829return false;830}831}832833@Override834public int hashCode() {835// from the "Effective Java: Programming Language Guide"836int result = 17;837result = 37 * result + start;838result = 37 * result + end;839return result;840}841842@Override843public String toString() {844return "IntegerSegment [" + start + ", " + end + "]";845}846}847}848849850