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