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      */
 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      */
 206     public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
 207                                                        throws PrinterException {
 208 
 209         // for easy access to these values
 210         final int imgWidth = (int)pageFormat.getImageableWidth();
 211         final int imgHeight = (int)pageFormat.getImageableHeight();
 212 
 213         if (imgWidth <= 0) {
 214             throw new PrinterException("Width of printable area is too small.");
 215         }
 216 
 217         // to pass the page number when formatting the header and footer text
 218         Object[] pageNumber = new Object[]{Integer.valueOf(pageIndex + 1)};
 219 
 220         // fetch the formatted header text, if any
 221         String headerText = null;
 222         if (headerFormat != null) {
 223             headerText = headerFormat.format(pageNumber);
 224         }
 225 
 226         // fetch the formatted footer text, if any
 227         String footerText = null;
 228         if (footerFormat != null) {
 229             footerText = footerFormat.format(pageNumber);
 230         }
 231 
 232         // to store the bounds of the header and footer text
 233         Rectangle2D hRect = null;
 234         Rectangle2D fRect = null;
 235 
 236         // the amount of vertical space needed for the header and footer text
 237         int headerTextSpace = 0;
 238         int footerTextSpace = 0;
 239 
 240         // the amount of vertical space available for printing the table
 241         int availableSpace = imgHeight;
 242 
 243         // if there's header text, find out how much space is needed for it
 244         // and subtract that from the available space
 245         if (headerText != null) {
 246             graphics.setFont(headerFont);
 247             hRect = graphics.getFontMetrics().getStringBounds(headerText,
 248                                                               graphics);
 249 
 250             headerTextSpace = (int)Math.ceil(hRect.getHeight());
 251             availableSpace -= headerTextSpace + H_F_SPACE;
 252         }
 253 
 254         // if there's footer text, find out how much space is needed for it
 255         // and subtract that from the available space
 256         if (footerText != null) {
 257             graphics.setFont(footerFont);
 258             fRect = graphics.getFontMetrics().getStringBounds(footerText,
 259                                                               graphics);
 260 
 261             footerTextSpace = (int)Math.ceil(fRect.getHeight());
 262             availableSpace -= footerTextSpace + H_F_SPACE;
 263         }
 264 
 265         if (availableSpace <= 0) {
 266             throw new PrinterException("Height of printable area is too small.");
 267         }
 268 
 269         // depending on the print mode, we may need a scale factor to
 270         // fit the table's entire width on the page
 271         double sf = 1.0D;
 272         if (printMode == JTable.PrintMode.FIT_WIDTH &&
 273                 totalColWidth > imgWidth) {
 274 
 275             // if not, we would have thrown an acception previously
 276             assert imgWidth > 0;
 277 
 278             // it must be, according to the if-condition, since imgWidth > 0
 279             assert totalColWidth > 1;
 280 
 281             sf = (double)imgWidth / (double)totalColWidth;
 282         }
 283 
 284         // dictated by the previous two assertions
 285         assert sf > 0;
 286 
 287         // This is in a loop for two reasons:
 288         // First, it allows us to catch up in case we're called starting
 289         // with a non-zero pageIndex. Second, we know that we can be called
 290         // for the same page multiple times. The condition of this while
 291         // loop acts as a check, ensuring that we don't attempt to do the
 292         // calculations again when we are called subsequent times for the
 293         // same page.
 294         while (last < pageIndex) {
 295             // if we are finished all columns in all rows
 296             if (row >= table.getRowCount() && col == 0) {
 297                 return NO_SUCH_PAGE;
 298             }
 299 
 300             // rather than multiplying every row and column by the scale factor
 301             // in findNextClip, just pass a width and height that have already
 302             // been divided by it
 303             int scaledWidth = (int)(imgWidth / sf);
 304             int scaledHeight = (int)((availableSpace - hclip.height) / sf);
 305 
 306             // calculate the area of the table to be printed for this page
 307             findNextClip(scaledWidth, scaledHeight);
 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 
 347         // if we have a scale factor, scale the graphics object to fit
 348         // the entire width
 349         if (sf != 1.0D) {
 350             g2d.scale(sf, sf);
 351 
 352         // otherwise, ensure that the current portion of the table is
 353         // centered horizontally
 354         } else {
 355             int diff = (imgWidth - clip.width) / 2;
 356             g2d.translate(diff, 0);
 357         }
 358 
 359         // store the old transform and clip for later restoration
 360         oldTrans = g2d.getTransform();
 361         Shape oldClip = g2d.getClip();
 362 
 363         // if there's a table header, print the current section and
 364         // then translate downwards
 365         if (header != null) {
 366             hclip.x = clip.x;
 367             hclip.width = clip.width;
 368 
 369             g2d.translate(-hclip.x, 0);
 370             g2d.clip(hclip);
 371             header.print(g2d);
 372 
 373             // restore the original transform and clip
 374             g2d.setTransform(oldTrans);
 375             g2d.setClip(oldClip);
 376 
 377             // translate downwards
 378             g2d.translate(0, hclip.height);
 379         }
 380 
 381         // print the current section of the table
 382         g2d.translate(-clip.x, -clip.y);
 383         g2d.clip(clip);
 384         table.print(g2d);
 385 
 386         // restore the original transform and clip
 387         g2d.setTransform(oldTrans);
 388         g2d.setClip(oldClip);
 389 
 390         // draw a box around the table
 391         g2d.setColor(Color.BLACK);
 392         g2d.drawRect(0, 0, clip.width, hclip.height + clip.height);
 393 
 394         // dispose the graphics copy
 395         g2d.dispose();
 396 
 397         return PAGE_EXISTS;
 398     }
 399 
 400     /**
 401      * A helper method that encapsulates common code for rendering the
 402      * header and footer text.
 403      *
 404      * @param  g2d       the graphics to draw into
 405      * @param  text      the text to draw, non null
 406      * @param  rect      the bounding rectangle for this text,
 407      *                   as calculated at the given font, non null
 408      * @param  font      the font to draw the text in, non null
 409      * @param  imgWidth  the width of the area to draw into
 410      */
 411     private void printText(Graphics2D g2d,
 412                            String text,
 413                            Rectangle2D rect,
 414                            Font font,
 415                            int imgWidth) {
 416 
 417             int tx;
 418 
 419             // if the text is small enough to fit, center it
 420             if (rect.getWidth() < imgWidth) {
 421                 tx = (int)((imgWidth - rect.getWidth()) / 2);
 422 
 423             // otherwise, if the table is LTR, ensure the left side of
 424             // the text shows; the right can be clipped
 425             } else if (table.getComponentOrientation().isLeftToRight()) {
 426                 tx = 0;
 427 
 428             // otherwise, ensure the right side of the text shows
 429             } else {
 430                 tx = -(int)(Math.ceil(rect.getWidth()) - imgWidth);
 431             }
 432 
 433             int ty = (int)Math.ceil(Math.abs(rect.getY()));
 434             g2d.setColor(Color.BLACK);
 435             g2d.setFont(font);
 436             g2d.drawString(text, tx, ty);
 437     }
 438 
 439     /**
 440      * Calculate the area of the table to be printed for
 441      * the next page. This should only be called if there
 442      * are rows and columns left to print.
 443      *
 444      * To avoid an infinite loop in printing, this will
 445      * always put at least one cell on each page.
 446      *
 447      * @param  pw  the width of the area to print in
 448      * @param  ph  the height of the area to print in
 449      */
 450     private void findNextClip(int pw, int ph) {
 451         final boolean ltr = table.getComponentOrientation().isLeftToRight();
 452 
 453         // if we're ready to start a new set of rows
 454         if (col == 0) {
 455             if (ltr) {
 456                 // adjust clip to the left of the first column
 457                 clip.x = 0;
 458             } else {
 459                 // adjust clip to the right of the first column
 460                 clip.x = totalColWidth;
 461             }
 462 
 463             // adjust clip to the top of the next set of rows
 464             clip.y += clip.height;
 465 
 466             // adjust clip width and height to be zero
 467             clip.width = 0;
 468             clip.height = 0;
 469 
 470             // fit as many rows as possible, and at least one
 471             int rowCount = table.getRowCount();
 472             int rowHeight = table.getRowHeight(row);
 473             do {
 474                 clip.height += rowHeight;
 475 
 476                 if (++row >= rowCount) {
 477                     break;
 478                 }
 479 
 480                 rowHeight = table.getRowHeight(row);
 481             } while (clip.height + rowHeight <= ph);
 482         }
 483 
 484         // we can short-circuit for JTable.PrintMode.FIT_WIDTH since
 485         // we'll always fit all columns on the page
 486         if (printMode == JTable.PrintMode.FIT_WIDTH) {
 487             clip.x = 0;
 488             clip.width = totalColWidth;
 489             return;
 490         }
 491 
 492         if (ltr) {
 493             // adjust clip to the left of the next set of columns
 494             clip.x += clip.width;
 495         }
 496 
 497         // adjust clip width to be zero
 498         clip.width = 0;
 499 
 500         // fit as many columns as possible, and at least one
 501         int colCount = table.getColumnCount();
 502         int colWidth = colModel.getColumn(col).getWidth();
 503         do {
 504             clip.width += colWidth;
 505             if (!ltr) {
 506                 clip.x -= colWidth;
 507             }
 508 
 509             if (++col >= colCount) {
 510                 // reset col to 0 to indicate we're finished all columns
 511                 col = 0;
 512 
 513                 break;
 514             }
 515 
 516             colWidth = colModel.getColumn(col).getWidth();
 517         } while (clip.width + colWidth <= pw);
 518 
 519     }
 520 
 521 }