1 /*
2  * Copyright (c) 2003, 2008, 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 
26 package javax.swing;
27 
28 import javax.swing.table.*;
29 import java.awt.*;
30 import java.awt.print.*;
31 import java.awt.geom.*;
32 import java.text.MessageFormat;
33 
34 /**
35  * An implementation of <code>Printable</code> for printing
36  * <code>JTable</code>s.
37  * <p>
38  * This implementation spreads table rows naturally in sequence
39  * across multiple pages, fitting as many rows as possible per page.
40  * The distribution of columns, on the other hand, is controlled by a
41  * printing mode parameter passed to the constructor. When
42  * <code>JTable.PrintMode.NORMAL</code> is used, the implementation
43  * handles columns in a similar manner to how it handles rows, spreading them
44  * across multiple pages (in an order consistent with the table's
45  * <code>ComponentOrientation</code>).
46  * When <code>JTable.PrintMode.FIT_WIDTH</code> is given, the implementation
47  * scales the output smaller if necessary, to ensure that all columns fit on
48  * the page. (Note that width and height are scaled equally, ensuring that the
49  * aspect ratio remains the same).
50  * <p>
51  * The portion of table printed on each page is headed by the
52  * appropriate section of the table's <code>JTableHeader</code>.
53  * <p>
54  * Header and footer text can be added to the output by providing
55  * <code>MessageFormat</code> instances to the constructor. The
56  * printing code requests Strings from the formats by calling
57  * their <code>format</code> method with a single parameter:
58  * an <code>Object</code> array containing a single element of type
59  * <code>Integer</code>, representing the current page number.
60  * <p>
61  * There are certain circumstances where this <code>Printable</code>
62  * cannot fit items appropriately, resulting in clipped output.
63  * These are:
64  * <ul>
65  *   <li>In any mode, when the header or footer text is too wide to
66  *       fit completely in the printable area. The implementation
67  *       prints as much of the text as possible starting from the beginning,
68  *       as determined by the table's <code>ComponentOrientation</code>.
69  *   <li>In any mode, when a row is too tall to fit in the
70  *       printable area. The upper most portion of the row
71  *       is printed and no lower border is shown.
72  *   <li>In <code>JTable.PrintMode.NORMAL</code> when a column
73  *       is too wide to fit in the printable area. The center of the
74  *       column is printed and no left and right borders are shown.
75  * </ul>
76  * <p>
77  * It is entirely valid for a developer to wrap this <code>Printable</code>
78  * inside another in order to create complex reports and documents. They may
79  * even request that different pages be rendered into different sized
80  * printable areas. The implementation was designed to handle this by
81  * performing most of its calculations on the fly. However, providing different
82  * sizes works best when <code>JTable.PrintMode.FIT_WIDTH</code> is used, or
83  * when only the printable width is changed between pages. This is because when
84  * it is printing a set of rows in <code>JTable.PrintMode.NORMAL</code> and the
85  * implementation determines a need to distribute columns across pages,
86  * it assumes that all of those rows will fit on each subsequent page needed
87  * to fit the columns.
88  * <p>
89  * It is the responsibility of the developer to ensure that the table is not
90  * modified in any way after this <code>Printable</code> is created (invalid
91  * modifications include changes in: size, renderers, or underlying data).
92  * The behavior of this <code>Printable</code> is undefined if the table is
93  * changed at any time after creation.
94  *
95  * @author  Shannon Hickey
96  */
97 class TablePrintable implements Printable {
98 
99     /** The table to print. */
100     private JTable table;
101 
102     /** For quick reference to the table's header. */
103     private JTableHeader header;
104 
105     /** For quick reference to the table's column model. */
106     private TableColumnModel colModel;
107 
108     /** To save multiple calculations of total column width. */
109     private int totalColWidth;
110 
111     /** The printing mode of this printable. */
112     private JTable.PrintMode printMode;
113 
114     /** Provides the header text for the table. */
115     private MessageFormat headerFormat;
116 
117     /** Provides the footer text for the table. */
118     private MessageFormat footerFormat;
119 
120     /** The most recent page index asked to print. */
121     private int last = -1;
122 
123     /** The next row to print. */
124     private int row = 0;
125 
126     /** The next column to print. */
127     private int col = 0;
128 
129     /** Used to store an area of the table to be printed. */
130     private final Rectangle clip = new Rectangle(0, 0, 0, 0);
131 
132     /** Used to store an area of the table's header to be printed. */
133     private final Rectangle hclip = new Rectangle(0, 0, 0, 0);
134 
135     /** Saves the creation of multiple rectangles. */
136     private final Rectangle tempRect = new Rectangle(0, 0, 0, 0);
137 
138     /** Vertical space to leave between table and header/footer text. */
139     private static final int H_F_SPACE = 8;
140 
141     /** Font size for the header text. */
142     private static final float HEADER_FONT_SIZE = 18.0f;
143 
144     /** Font size for the footer text. */
145     private static final float FOOTER_FONT_SIZE = 12.0f;
146 
147     /** The font to use in rendering header text. */
148     private Font headerFont;
149 
150     /** The font to use in rendering footer text. */
151     private Font footerFont;
152 
153     /**
154      * Create a new <code>TablePrintable</code> for the given
155      * <code>JTable</code>. Header and footer text can be specified using the
156      * two <code>MessageFormat</code> parameters. When called upon to provide
157      * a String, each format is given the current page number.
158      *
159      * @param  table         the table to print
160      * @param  printMode     the printing mode for this printable
161      * @param  headerFormat  a <code>MessageFormat</code> specifying the text to
162      *                       be used in printing a header, or null for none
163      * @param  footerFormat  a <code>MessageFormat</code> specifying the text to
164      *                       be used in printing a footer, or null for none
165      * @throws IllegalArgumentException if passed an invalid print mode
166      */
TablePrintable(JTable table, JTable.PrintMode printMode, MessageFormat headerFormat, MessageFormat footerFormat)167     public TablePrintable(JTable table,
168                           JTable.PrintMode printMode,
169                           MessageFormat headerFormat,
170                           MessageFormat footerFormat) {
171 
172         this.table = table;
173 
174         header = table.getTableHeader();
175         colModel = table.getColumnModel();
176         totalColWidth = colModel.getTotalColumnWidth();
177 
178         if (header != null) {
179             // the header clip height can be set once since it's unchanging
180             hclip.height = header.getHeight();
181         }
182 
183         this.printMode = printMode;
184 
185         this.headerFormat = headerFormat;
186         this.footerFormat = footerFormat;
187 
188         // derive the header and footer font from the table's font
189         headerFont = table.getFont().deriveFont(Font.BOLD,
190                                                 HEADER_FONT_SIZE);
191         footerFont = table.getFont().deriveFont(Font.PLAIN,
192                                                 FOOTER_FONT_SIZE);
193     }
194 
195     /**
196      * Prints the specified page of the table into the given {@link Graphics}
197      * context, in the specified format.
198      *
199      * @param   graphics    the context into which the page is drawn
200      * @param   pageFormat  the size and orientation of the page being drawn
201      * @param   pageIndex   the zero based index of the page to be drawn
202      * @return  PAGE_EXISTS if the page is rendered successfully, or
203      *          NO_SUCH_PAGE if a non-existent page index is specified
204      * @throws  PrinterException if an error causes printing to be aborted
205      */
print(Graphics graphics, PageFormat pageFormat, int pageIndex)206     public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
207                                                        throws PrinterException {
208         // for easy access to these values
209         final int imgWidth = (int)pageFormat.getImageableWidth();
210         final int imgHeight = (int)pageFormat.getImageableHeight();
211         if (imgWidth <= 0) {
212             throw new PrinterException("Width of printable area is too small.");
213         }
214 
215         // to pass the page number when formatting the header and footer text
216         Object[] pageNumber = new Object[]{Integer.valueOf(pageIndex + 1)};
217 
218         // fetch the formatted header text, if any
219         String headerText = null;
220         if (headerFormat != null) {
221             headerText = headerFormat.format(pageNumber);
222         }
223 
224         // fetch the formatted footer text, if any
225         String footerText = null;
226         if (footerFormat != null) {
227             footerText = footerFormat.format(pageNumber);
228         }
229 
230         // to store the bounds of the header and footer text
231         Rectangle2D hRect = null;
232         Rectangle2D fRect = null;
233 
234         // the amount of vertical space needed for the header and footer text
235         int headerTextSpace = 0;
236         int footerTextSpace = 0;
237 
238         // the amount of vertical space available for printing the table
239         int availableSpace = imgHeight;
240 
241         // if there's header text, find out how much space is needed for it
242         // and subtract that from the available space
243         if (headerText != null) {
244             graphics.setFont(headerFont);
245             hRect = graphics.getFontMetrics().getStringBounds(headerText,
246                                                               graphics);
247 
248             headerTextSpace = (int)Math.ceil(hRect.getHeight());
249             availableSpace -= headerTextSpace + H_F_SPACE;
250         }
251 
252         // if there's footer text, find out how much space is needed for it
253         // and subtract that from the available space
254         if (footerText != null) {
255             graphics.setFont(footerFont);
256             fRect = graphics.getFontMetrics().getStringBounds(footerText,
257                                                               graphics);
258 
259             footerTextSpace = (int)Math.ceil(fRect.getHeight());
260             availableSpace -= footerTextSpace + H_F_SPACE;
261         }
262 
263         if (availableSpace <= 0) {
264             throw new PrinterException("Height of printable area is too small.");
265         }
266 
267         // depending on the print mode, we may need a scale factor to
268         // fit the table's entire width on the page
269         double sf = 1.0D;
270         if (printMode == JTable.PrintMode.FIT_WIDTH &&
271                 totalColWidth > imgWidth) {
272 
273             // if not, we would have thrown an acception previously
274             assert imgWidth > 0;
275 
276             // it must be, according to the if-condition, since imgWidth > 0
277             assert totalColWidth > 1;
278 
279             sf = (double)imgWidth / (double)totalColWidth;
280         }
281 
282         // dictated by the previous two assertions
283         assert sf > 0;
284 
285         // This is in a loop for two reasons:
286         // First, it allows us to catch up in case we're called starting
287         // with a non-zero pageIndex. Second, we know that we can be called
288         // for the same page multiple times. The condition of this while
289         // loop acts as a check, ensuring that we don't attempt to do the
290         // calculations again when we are called subsequent times for the
291         // same page.
292         while (last < pageIndex) {
293             // if we are finished all columns in all rows
294             if (row >= table.getRowCount() && col == 0) {
295                 return NO_SUCH_PAGE;
296             }
297 
298             // rather than multiplying every row and column by the scale factor
299             // in findNextClip, just pass a width and height that have already
300             // been divided by it
301             int scaledWidth = (int)(imgWidth / sf);
302             int scaledHeight = (int)((availableSpace - hclip.height) / sf);
303             // calculate the area of the table to be printed for this page
304             findNextClip(scaledWidth, scaledHeight);
305 
306             if (!((table.getBounds()).intersects(clip))) {
307                 return NO_SUCH_PAGE;
308             }
309             last++;
310         }
311 
312         // create a copy of the graphics so we don't affect the one given to us
313         Graphics2D g2d = (Graphics2D)graphics.create();
314 
315         // translate into the co-ordinate system of the pageFormat
316         g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
317 
318         // to save and store the transform
319         AffineTransform oldTrans;
320 
321         // if there's footer text, print it at the bottom of the imageable area
322         if (footerText != null) {
323             oldTrans = g2d.getTransform();
324 
325             g2d.translate(0, imgHeight - footerTextSpace);
326 
327             printText(g2d, footerText, fRect, footerFont, imgWidth);
328 
329             g2d.setTransform(oldTrans);
330         }
331 
332         // if there's header text, print it at the top of the imageable area
333         // and then translate downwards
334         if (headerText != null) {
335             printText(g2d, headerText, hRect, headerFont, imgWidth);
336 
337             g2d.translate(0, headerTextSpace + H_F_SPACE);
338         }
339 
340         // constrain the table output to the available space
341         tempRect.x = 0;
342         tempRect.y = 0;
343         tempRect.width = imgWidth;
344         tempRect.height = availableSpace;
345         g2d.clip(tempRect);
346         // if we have a scale factor, scale the graphics object to fit
347         // the entire width
348         if (sf != 1.0D) {
349             g2d.scale(sf, sf);
350 
351         // otherwise, ensure that the current portion of the table is
352         // centered horizontally
353         } else {
354             int diff = (imgWidth - clip.width) / 2;
355             g2d.translate(diff, 0);
356         }
357 
358         // store the old transform and clip for later restoration
359         oldTrans = g2d.getTransform();
360         Shape oldClip = g2d.getClip();
361 
362         // if there's a table header, print the current section and
363         // then translate downwards
364         if (header != null) {
365             hclip.x = clip.x;
366             hclip.width = clip.width;
367 
368             g2d.translate(-hclip.x, 0);
369             g2d.clip(hclip);
370             header.print(g2d);
371 
372             // restore the original transform and clip
373             g2d.setTransform(oldTrans);
374             g2d.setClip(oldClip);
375 
376             // translate downwards
377             g2d.translate(0, hclip.height);
378         }
379 
380         // print the current section of the table
381         g2d.translate(-clip.x, -clip.y);
382         g2d.clip(clip);
383 
384         // set a property so that BasicTableUI#paint can know JTable printMode
385         // is FIT_WIDTH since TablePrintable.printMode is not accessible from BasicTableUI
386         if (printMode == JTable.PrintMode.FIT_WIDTH) {
387             table.putClientProperty("Table.printMode", JTable.PrintMode.FIT_WIDTH);
388         }
389         table.print(g2d);
390 
391         // restore the original transform and clip
392         g2d.setTransform(oldTrans);
393         g2d.setClip(oldClip);
394 
395         // draw a box around the table
396         g2d.setColor(Color.BLACK);
397 
398         // compute the visible portion of table and draw the rect around it
399         Rectangle visibleBounds = clip.intersection(table.getBounds());
400         Point upperLeft = visibleBounds.getLocation();
401         Point lowerRight = new Point(visibleBounds.x + visibleBounds.width,
402                                      visibleBounds.y + visibleBounds.height);
403 
404         int rMin = table.rowAtPoint(upperLeft);
405         int rMax = table.rowAtPoint(lowerRight);
406         if (rMin == -1) {
407             rMin = 0;
408         }
409         if (rMax == -1) {
410             rMax = table.getRowCount();
411         }
412         int rowHeight = 0;
413         for(int visrow = rMin; visrow < rMax; visrow++) {
414             rowHeight += table.getRowHeight(visrow);
415         }
416         // If PrintMode is FIT_WIDTH, then draw rect for entire column width while
417         // printing irrespective of how many columns are visible in console
418         if (printMode == JTable.PrintMode.FIT_WIDTH) {
419             g2d.drawRect(0, 0, clip.width, hclip.height + rowHeight);
420         } else {
421             g2d.drawRect(0, 0, visibleBounds.width, hclip.height + rowHeight);
422         }
423 
424         // clear the property
425         if (printMode == JTable.PrintMode.FIT_WIDTH) {
426             table.putClientProperty("Table.printMode", null);
427         }
428         // dispose the graphics copy
429         g2d.dispose();
430 
431         return PAGE_EXISTS;
432     }
433 
434     /**
435      * A helper method that encapsulates common code for rendering the
436      * header and footer text.
437      *
438      * @param  g2d       the graphics to draw into
439      * @param  text      the text to draw, non null
440      * @param  rect      the bounding rectangle for this text,
441      *                   as calculated at the given font, non null
442      * @param  font      the font to draw the text in, non null
443      * @param  imgWidth  the width of the area to draw into
444      */
printText(Graphics2D g2d, String text, Rectangle2D rect, Font font, int imgWidth)445     private void printText(Graphics2D g2d,
446                            String text,
447                            Rectangle2D rect,
448                            Font font,
449                            int imgWidth) {
450 
451             int tx;
452 
453             // if the text is small enough to fit, center it
454             if (rect.getWidth() < imgWidth) {
455                 tx = (int)((imgWidth - rect.getWidth()) / 2);
456 
457             // otherwise, if the table is LTR, ensure the left side of
458             // the text shows; the right can be clipped
459             } else if (table.getComponentOrientation().isLeftToRight()) {
460                 tx = 0;
461 
462             // otherwise, ensure the right side of the text shows
463             } else {
464                 tx = -(int)(Math.ceil(rect.getWidth()) - imgWidth);
465             }
466 
467             int ty = (int)Math.ceil(Math.abs(rect.getY()));
468             g2d.setColor(Color.BLACK);
469             g2d.setFont(font);
470             g2d.drawString(text, tx, ty);
471     }
472 
473     /**
474      * Calculate the area of the table to be printed for
475      * the next page. This should only be called if there
476      * are rows and columns left to print.
477      *
478      * To avoid an infinite loop in printing, this will
479      * always put at least one cell on each page.
480      *
481      * @param  pw  the width of the area to print in
482      * @param  ph  the height of the area to print in
483      */
findNextClip(int pw, int ph)484     private void findNextClip(int pw, int ph) {
485         final boolean ltr = table.getComponentOrientation().isLeftToRight();
486 
487         // if we're ready to start a new set of rows
488         if (col == 0) {
489             if (ltr) {
490                 // adjust clip to the left of the first column
491                 clip.x = 0;
492             } else {
493                 // adjust clip to the right of the first column
494                 clip.x = totalColWidth;
495             }
496 
497             // adjust clip to the top of the next set of rows
498             clip.y += clip.height;
499 
500             // adjust clip width and height to be zero
501             clip.width = 0;
502             clip.height = 0;
503 
504             // fit as many rows as possible, and at least one
505             int rowCount = table.getRowCount();
506             int rowHeight = table.getRowHeight(row);
507             do {
508                 clip.height += rowHeight;
509 
510                 if (++row >= rowCount) {
511                     break;
512                 }
513 
514                 rowHeight = table.getRowHeight(row);
515             } while (clip.height + rowHeight <= ph);
516         }
517 
518         // we can short-circuit for JTable.PrintMode.FIT_WIDTH since
519         // we'll always fit all columns on the page
520         if (printMode == JTable.PrintMode.FIT_WIDTH) {
521             clip.x = 0;
522             clip.width = totalColWidth;
523             return;
524         }
525 
526         if (ltr) {
527             // adjust clip to the left of the next set of columns
528             clip.x += clip.width;
529         }
530 
531         // adjust clip width to be zero
532         clip.width = 0;
533 
534         // fit as many columns as possible, and at least one
535         int colCount = table.getColumnCount();
536         int colWidth = colModel.getColumn(col).getWidth();
537         do {
538             clip.width += colWidth;
539             if (!ltr) {
540                 clip.x -= colWidth;
541             }
542 
543             if (++col >= colCount) {
544                 // reset col to 0 to indicate we're finished all columns
545                 col = 0;
546                 break;
547             }
548 
549             colWidth = colModel.getColumn(col).getWidth();
550         } while (clip.width + colWidth <= pw);
551 
552     }
553 }
554