Path: blob/aarch64-shenandoah-jdk8u272-b10/jdk/src/share/classes/sun/swing/text/TextComponentPrintable.java
38918 views
/*1* Copyright (c) 2005, 2011, 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 c 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}326private JTextComponent createPrintShellOnEDT(final JTextComponent textComponent) {327assert SwingUtilities.isEventDispatchThread();328329JTextComponent ret = null;330if (textComponent instanceof JPasswordField) {331ret =332new JPasswordField() {333{334setEchoChar(((JPasswordField) textComponent).getEchoChar());335setHorizontalAlignment(336((JTextField) textComponent).getHorizontalAlignment());337}338@Override339public FontMetrics getFontMetrics(Font font) {340return (frc.get() == null)341? super.getFontMetrics(font)342: FontDesignMetrics.getMetrics(font, frc.get());343}344};345} else if (textComponent instanceof JTextField) {346ret =347new JTextField() {348{349setHorizontalAlignment(350((JTextField) textComponent).getHorizontalAlignment());351}352@Override353public FontMetrics getFontMetrics(Font font) {354return (frc.get() == null)355? super.getFontMetrics(font)356: FontDesignMetrics.getMetrics(font, frc.get());357}358};359} else if (textComponent instanceof JTextArea) {360ret =361new JTextArea() {362{363JTextArea textArea = (JTextArea) textComponent;364setLineWrap(textArea.getLineWrap());365setWrapStyleWord(textArea.getWrapStyleWord());366setTabSize(textArea.getTabSize());367}368@Override369public FontMetrics getFontMetrics(Font font) {370return (frc.get() == null)371? super.getFontMetrics(font)372: FontDesignMetrics.getMetrics(font, frc.get());373}374};375} else if (textComponent instanceof JTextPane) {376ret =377new JTextPane() {378@Override379public FontMetrics getFontMetrics(Font font) {380return (frc.get() == null)381? super.getFontMetrics(font)382: FontDesignMetrics.getMetrics(font, frc.get());383}384@Override385public EditorKit getEditorKit() {386if (getDocument() == textComponent.getDocument()) {387return ((JTextPane) textComponent).getEditorKit();388} else {389return super.getEditorKit();390}391}392};393} else if (textComponent instanceof JEditorPane) {394ret =395new JEditorPane() {396@Override397public FontMetrics getFontMetrics(Font font) {398return (frc.get() == null)399? super.getFontMetrics(font)400: FontDesignMetrics.getMetrics(font, frc.get());401}402@Override403public EditorKit getEditorKit() {404if (getDocument() == textComponent.getDocument()) {405return ((JEditorPane) textComponent).getEditorKit();406} else {407return super.getEditorKit();408}409}410};411}412//want to occupy the whole width and height by text413ret.setBorder(null);414415//set properties from the component to print416ret.setOpaque(textComponent.isOpaque());417ret.setEditable(textComponent.isEditable());418ret.setEnabled(textComponent.isEnabled());419ret.setFont(textComponent.getFont());420ret.setBackground(textComponent.getBackground());421ret.setForeground(textComponent.getForeground());422ret.setComponentOrientation(423textComponent.getComponentOrientation());424425if (ret instanceof JEditorPane) {426ret.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES,427textComponent.getClientProperty(428JEditorPane.HONOR_DISPLAY_PROPERTIES));429ret.putClientProperty(JEditorPane.W3C_LENGTH_UNITS,430textComponent.getClientProperty(JEditorPane.W3C_LENGTH_UNITS));431ret.putClientProperty("charset",432textComponent.getClientProperty("charset"));433}434ret.setDocument(textComponent.getDocument());435return ret;436}437438439440441/**442* Returns the number of pages in this printable.443* <p>444* This number is defined only after {@code print} returns NO_SUCH_PAGE.445*446* @return the number of pages.447*/448public int getNumberOfPages() {449return pagesMetrics.size();450}451452/**453* See Printable.print for the API description.454*455* There are two parts in the implementation.456* First part (print) is to be called on the printing thread.457* Second part (printOnEDT) is to be called on the EDT only.458*459* print triggers printOnEDT460*/461public int print(final Graphics graphics,462final PageFormat pf,463final int pageIndex) throws PrinterException {464if (!isLayouted) {465if (graphics instanceof Graphics2D) {466frc.set(((Graphics2D)graphics).getFontRenderContext());467}468layout((int)Math.floor(pf.getImageableWidth()));469calculateRowsMetrics();470}471int ret;472if (!SwingUtilities.isEventDispatchThread()) {473Callable<Integer> doPrintOnEDT = new Callable<Integer>() {474public Integer call() throws Exception {475return printOnEDT(graphics, pf, pageIndex);476}477};478FutureTask<Integer> futurePrintOnEDT =479new FutureTask<Integer>(doPrintOnEDT);480SwingUtilities.invokeLater(futurePrintOnEDT);481try {482ret = futurePrintOnEDT.get();483} catch (InterruptedException e) {484throw new RuntimeException(e);485} catch (ExecutionException e) {486Throwable cause = e.getCause();487if (cause instanceof PrinterException) {488throw (PrinterException)cause;489} else if (cause instanceof RuntimeException) {490throw (RuntimeException) cause;491} else if (cause instanceof Error) {492throw (Error) cause;493} else {494throw new RuntimeException(cause);495}496}497} else {498ret = printOnEDT(graphics, pf, pageIndex);499}500return ret;501}502503504/**505* The EDT part of the print method.506*507* This method is to be called on the EDT only. Layout should be done before508* calling this method.509*/510private int printOnEDT(final Graphics graphics,511final PageFormat pf,512final int pageIndex) throws PrinterException {513assert SwingUtilities.isEventDispatchThread();514Border border = BorderFactory.createEmptyBorder();515//handle header and footer516if (headerFormat != null || footerFormat != null) {517//Printable page enumeration is 0 base. We need 1 based.518Object[] formatArg = new Object[]{Integer.valueOf(pageIndex + 1)};519if (headerFormat != null) {520border = new TitledBorder(border,521headerFormat.format(formatArg),522TitledBorder.CENTER, TitledBorder.ABOVE_TOP,523headerFont, printShell.getForeground());524}525if (footerFormat != null) {526border = new TitledBorder(border,527footerFormat.format(formatArg),528TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM,529footerFont, printShell.getForeground());530}531}532Insets borderInsets = border.getBorderInsets(printShell);533updatePagesMetrics(pageIndex,534(int)Math.floor(pf.getImageableHeight()) - borderInsets.top535- borderInsets.bottom);536537if (pagesMetrics.size() <= pageIndex) {538return NO_SUCH_PAGE;539}540541Graphics2D g2d = (Graphics2D)graphics.create();542543g2d.translate(pf.getImageableX(), pf.getImageableY());544border.paintBorder(printShell, g2d, 0, 0,545(int)Math.floor(pf.getImageableWidth()),546(int)Math.floor(pf.getImageableHeight()));547548g2d.translate(0, borderInsets.top);549//want to clip only vertically550Rectangle clip = new Rectangle(0, 0,551(int) pf.getWidth(),552pagesMetrics.get(pageIndex).end553- pagesMetrics.get(pageIndex).start + 1);554555g2d.clip(clip);556int xStart = 0;557if (ComponentOrientation.RIGHT_TO_LEFT ==558printShell.getComponentOrientation()) {559xStart = (int) pf.getImageableWidth() - printShell.getWidth();560}561g2d.translate(xStart, - pagesMetrics.get(pageIndex).start);562printShell.print(g2d);563564g2d.dispose();565566return Printable.PAGE_EXISTS;567}568569570private boolean needReadLock = false;571572/**573* Tries to release document's readlock574*575* Note: Not to be called on the EDT.576*/577private void releaseReadLock() {578assert ! SwingUtilities.isEventDispatchThread();579Document document = textComponentToPrint.getDocument();580if (document instanceof AbstractDocument) {581try {582((AbstractDocument) document).readUnlock();583needReadLock = true;584} catch (Error ignore) {585// readUnlock() might throw StateInvariantError586}587}588}589590591/**592* Tries to acquire document's readLock if it was released593* in releaseReadLock() method.594*595* Note: Not to be called on the EDT.596*/597private void acquireReadLock() {598assert ! SwingUtilities.isEventDispatchThread();599if (needReadLock) {600try {601/*602* wait until all the EDT events are processed603* some of the document changes are asynchronous604* we need to make sure we get the lock after those changes605*/606SwingUtilities.invokeAndWait(607new Runnable() {608public void run() {609}610});611} catch (InterruptedException ignore) {612} catch (java.lang.reflect.InvocationTargetException ignore) {613}614Document document = textComponentToPrint.getDocument();615((AbstractDocument) document).readLock();616needReadLock = false;617}618}619620/**621* Prepares {@code printShell} for printing.622*623* Sets properties from the component to print.624* Sets width and FontRenderContext.625*626* Triggers Views creation for the printShell.627*628* There are two parts in the implementation.629* First part (layout) is to be called on the printing thread.630* Second part (layoutOnEDT) is to be called on the EDT only.631*632* {@code layout} triggers {@code layoutOnEDT}.633*634* @param width width to layout the text for635*/636private void layout(final int width) {637if (!SwingUtilities.isEventDispatchThread()) {638Callable<Object> doLayoutOnEDT = new Callable<Object>() {639public Object call() throws Exception {640layoutOnEDT(width);641return null;642}643};644FutureTask<Object> futureLayoutOnEDT = new FutureTask<Object>(645doLayoutOnEDT);646647/*648* We need to release document's readlock while printShell is649* initializing650*/651releaseReadLock();652SwingUtilities.invokeLater(futureLayoutOnEDT);653try {654futureLayoutOnEDT.get();655} catch (InterruptedException e) {656throw new RuntimeException(e);657} catch (ExecutionException e) {658Throwable cause = e.getCause();659if (cause instanceof RuntimeException) {660throw (RuntimeException) cause;661} else if (cause instanceof Error) {662throw (Error) cause;663} else {664throw new RuntimeException(cause);665}666} finally {667acquireReadLock();668}669} else {670layoutOnEDT(width);671}672673isLayouted = true;674}675676/**677* The EDT part of layout method.678*679* This method is to be called on the EDT only.680*/681private void layoutOnEDT(final int width) {682assert SwingUtilities.isEventDispatchThread();683//need to have big value but smaller than MAX_VALUE otherwise684//printing goes south due to overflow somewhere685final int HUGE_INTEGER = Integer.MAX_VALUE - 1000;686687CellRendererPane rendererPane = new CellRendererPane();688689//need to use JViewport since text is layouted to the viewPort width690//otherwise it will be layouted to the maximum text width691JViewport viewport = new JViewport();692viewport.setBorder(null);693Dimension size = new Dimension(width, HUGE_INTEGER);694695//JTextField is a special case696//it layouts text in the middle by Y697if (printShell instanceof JTextField) {698size =699new Dimension(size.width, printShell.getPreferredSize().height);700}701printShell.setSize(size);702viewport.setComponentOrientation(printShell.getComponentOrientation());703viewport.setSize(size);704viewport.add(printShell);705rendererPane.add(viewport);706}707708/**709* Calculates pageMetrics for the pages up to the {@code pageIndex} using710* {@code rowsMetrics}.711* Metrics are stored in the {@code pagesMetrics}.712*713* @param pageIndex the page to update the metrics for714* @param pageHeight the page height715*/716private void updatePagesMetrics(final int pageIndex, final int pageHeight) {717while (pageIndex >= pagesMetrics.size() && !rowsMetrics.isEmpty()) {718// add one page to the pageMetrics719int lastPage = pagesMetrics.size() - 1;720int pageStart = (lastPage >= 0)721? pagesMetrics.get(lastPage).end + 1722: 0;723int rowIndex;724for (rowIndex = 0;725rowIndex < rowsMetrics.size()726&& (rowsMetrics.get(rowIndex).end - pageStart + 1)727<= pageHeight;728rowIndex++) {729}730if (rowIndex == 0) {731// can not fit a single row732// need to split733pagesMetrics.add(734new IntegerSegment(pageStart, pageStart + pageHeight - 1));735} else {736rowIndex--;737pagesMetrics.add(new IntegerSegment(pageStart,738rowsMetrics.get(rowIndex).end));739for (int i = 0; i <= rowIndex; i++) {740rowsMetrics.remove(0);741}742}743}744}745746/**747* Calculates rowsMetrics for the document. The result is stored748* in the {@code rowsMetrics}.749*750* Two steps process.751* First step is to find yStart and yEnd for the every document position.752* Second step is to merge all intersected segments ( [yStart, yEnd] ).753*/754private void calculateRowsMetrics() {755final int documentLength = printShell.getDocument().getLength();756List<IntegerSegment> documentMetrics = new ArrayList<IntegerSegment>(LIST_SIZE);757Rectangle rect;758for (int i = 0, previousY = -1, previousHeight = -1; i < documentLength;759i++) {760try {761rect = printShell.modelToView(i);762if (rect != null) {763int y = (int) rect.getY();764int height = (int) rect.getHeight();765if (height != 0766&& (y != previousY || height != previousHeight)) {767/*768* we do not store the same value as previous. in our769* documents it is often for consequent positons to have770* the same modelToView y and height.771*/772previousY = y;773previousHeight = height;774documentMetrics.add(new IntegerSegment(y, y + height - 1));775}776}777} catch (BadLocationException e) {778assert false;779}780}781/*782* Merge all intersected segments.783*/784Collections.sort(documentMetrics);785int yStart = Integer.MIN_VALUE;786int yEnd = Integer.MIN_VALUE;787for (IntegerSegment segment : documentMetrics) {788if (yEnd < segment.start) {789if (yEnd != Integer.MIN_VALUE) {790rowsMetrics.add(new IntegerSegment(yStart, yEnd));791}792yStart = segment.start;793yEnd = segment.end;794} else {795yEnd = segment.end;796}797}798if (yEnd != Integer.MIN_VALUE) {799rowsMetrics.add(new IntegerSegment(yStart, yEnd));800}801}802803/**804* Class to represent segment of integers.805* we do not call it Segment to avoid confusion with806* javax.swing.text.Segment807*/808private static class IntegerSegment implements Comparable<IntegerSegment> {809final int start;810final int end;811812IntegerSegment(int start, int end) {813this.start = start;814this.end = end;815}816817public int compareTo(IntegerSegment object) {818int startsDelta = start - object.start;819return (startsDelta != 0) ? startsDelta : end - object.end;820}821822@Override823public boolean equals(Object obj) {824if (obj instanceof IntegerSegment) {825return compareTo((IntegerSegment) obj) == 0;826} else {827return false;828}829}830831@Override832public int hashCode() {833// from the "Effective Java: Programming Language Guide"834int result = 17;835result = 37 * result + start;836result = 37 * result + end;837return result;838}839840@Override841public String toString() {842return "IntegerSegment [" + start + ", " + end + "]";843}844}845}846847848