001package jmri.util.davidflanagan; 002 003import java.awt.*; 004import java.awt.JobAttributes.DefaultSelectionType; 005import java.awt.event.ActionEvent; 006import java.io.IOException; 007import java.io.Writer; 008import java.text.DateFormat; 009import java.util.Date; 010import java.util.TimeZone; 011import java.util.Vector; 012 013import javax.swing.*; 014import javax.swing.border.EmptyBorder; 015 016import jmri.util.JmriJFrame; 017 018/** 019 * Provide graphic output to a screen/printer. 020 * <p> 021 * This is from Chapter 12 of the O'Reilly Java book by David Flanagan with the 022 * alligator on the front. 023 * 024 * @author David Flanagan 025 * @author Dennis Miller 026 */ 027public class HardcopyWriter extends Writer { 028 029 // instance variables 030 protected PrintJob job; 031 protected Graphics page; 032 protected String jobname; 033 protected String line; 034 protected int fontsize; 035 protected String time; 036 protected Dimension pagesize = new Dimension(612, 792); 037 protected int pagedpi = 72; 038 protected Font font, headerfont; 039 protected String fontName = "Monospaced"; 040 protected int fontStyle = Font.PLAIN; 041 protected FontMetrics metrics; 042 protected FontMetrics headermetrics; 043 protected int x0, y0; 044 protected int height, width; 045 protected int headery; 046 protected int charwidth; 047 protected int lineheight; 048 protected int lineascent; 049 protected int chars_per_line; 050 protected int lines_per_page; 051 protected int charnum = 0, linenum = 0; 052 protected int charoffset = 0; 053 protected int pagenum = 0; 054 protected int prFirst = 1; 055 protected Color color = Color.black; 056 protected boolean printHeader = true; 057 058 protected boolean isPreview; 059 protected Image previewImage; 060 protected Vector<Image> pageImages = new Vector<>(3, 3); 061 protected JmriJFrame previewFrame; 062 protected JPanel previewPanel; 063 protected ImageIcon previewIcon = new ImageIcon(); 064 protected JLabel previewLabel = new JLabel(); 065 protected JToolBar previewToolBar = new JToolBar(); 066 protected Frame frame; 067 protected JButton nextButton; 068 protected JButton previousButton; 069 protected JButton closeButton; 070 protected JLabel pageCount = new JLabel(); 071 072 // save state between invocations of write() 073 private boolean last_char_was_return = false; 074 075 // A static variable to hold prefs between print jobs 076 // private static Properties printprops = new Properties(); 077 // Job and Page attributes 078 JobAttributes jobAttributes = new JobAttributes(); 079 PageAttributes pageAttributes = new PageAttributes(); 080 081 // constructor modified to add print preview parameter 082 public HardcopyWriter(Frame frame, String jobname, int fontsize, double leftmargin, double rightmargin, 083 double topmargin, double bottommargin, boolean preview) throws HardcopyWriter.PrintCanceledException { 084 hardcopyWriter(frame, jobname, fontsize, leftmargin, rightmargin, topmargin, bottommargin, preview); 085 } 086 087 // constructor modified to add default printer name and page orientation 088 public HardcopyWriter(Frame frame, String jobname, int fontsize, double leftmargin, double rightmargin, 089 double topmargin, double bottommargin, boolean preview, String printerName, boolean landscape, 090 boolean printHeader, Dimension pagesize) throws HardcopyWriter.PrintCanceledException { 091 092 // print header? 093 this.printHeader = printHeader; 094 095 // set default print name 096 jobAttributes.setPrinter(printerName); 097 if (landscape) { 098 pageAttributes.setOrientationRequested(PageAttributes.OrientationRequestedType.LANDSCAPE); 099 if (preview) { 100 this.pagesize = new Dimension(792, 612); 101 } 102 } else if (preview && pagesize != null) { 103 this.pagesize = pagesize; 104 } 105 106 hardcopyWriter(frame, jobname, fontsize, leftmargin, rightmargin, topmargin, bottommargin, preview); 107 } 108 109 private void hardcopyWriter(Frame frame, String jobname, int fontsize, double leftmargin, double rightmargin, 110 double topmargin, double bottommargin, boolean preview) throws HardcopyWriter.PrintCanceledException { 111 112 isPreview = preview; 113 this.frame = frame; 114 115 // set default to color 116 pageAttributes.setColor(PageAttributes.ColorType.COLOR); 117 118 // skip printer selection if preview 119 if (!isPreview) { 120 Toolkit toolkit = frame.getToolkit(); 121 122 job = toolkit.getPrintJob(frame, jobname, jobAttributes, pageAttributes); 123 124 if (job == null) { 125 throw new PrintCanceledException("User cancelled print request"); 126 } 127 pagesize = job.getPageDimension(); 128 pagedpi = job.getPageResolution(); 129 // determine if user selected a range of pages to print out, note that page becomes null if range 130 // selected is less than the total number of pages, that's the reason for the page null checks 131 if (jobAttributes.getDefaultSelection().equals(DefaultSelectionType.RANGE)) { 132 prFirst = jobAttributes.getPageRanges()[0][0]; 133 } 134 } 135 136 x0 = (int) (leftmargin * pagedpi); 137 y0 = (int) (topmargin * pagedpi); 138 width = pagesize.width - (int) ((leftmargin + rightmargin) * pagedpi); 139 height = pagesize.height - (int) ((topmargin + bottommargin) * pagedpi); 140 141 // get body font and font size 142 font = new Font(fontName, fontStyle, fontsize); 143 metrics = frame.getFontMetrics(font); 144 lineheight = metrics.getHeight(); 145 lineascent = metrics.getAscent(); 146 charwidth = metrics.charWidth('m'); 147 148 // compute lines and columns within margins 149 chars_per_line = width / charwidth; 150 lines_per_page = height / lineheight; 151 152 // header font info 153 headerfont = new Font("SansSerif", Font.ITALIC, fontsize); 154 headermetrics = frame.getFontMetrics(headerfont); 155 headery = y0 - (int) (0.125 * pagedpi) - headermetrics.getHeight() + headermetrics.getAscent(); 156 157 // compute date/time for header 158 DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT); 159 df.setTimeZone(TimeZone.getDefault()); 160 time = df.format(new Date()); 161 162 this.jobname = jobname; 163 this.fontsize = fontsize; 164 165 if (isPreview) { 166 previewFrame = new JmriJFrame(Bundle.getMessage("PrintPreviewTitle") + " " + jobname); 167 previewFrame.getContentPane().setLayout(new BorderLayout()); 168 toolBarInit(); 169 previewToolBar.setFloatable(false); 170 previewFrame.getContentPane().add(previewToolBar, BorderLayout.NORTH); 171 previewPanel = new JPanel(); 172 previewPanel.setSize(pagesize.width, pagesize.height); 173 // add the panel to the frame and make visible, otherwise creating the image will fail. 174 // use a scroll pane to handle print images bigger than the window 175 previewFrame.getContentPane().add(new JScrollPane(previewPanel), BorderLayout.CENTER); 176 // page width 660 for portrait 177 previewFrame.setSize(pagesize.width + 48, pagesize.height + 100); 178 previewFrame.setVisible(true); 179 } 180 181 } 182 183 /** 184 * Create a print preview toolbar. 185 */ 186 protected void toolBarInit() { 187 previousButton = new JButton(Bundle.getMessage("ButtonPreviousPage")); 188 previewToolBar.add(previousButton); 189 previousButton.addActionListener((ActionEvent actionEvent) -> { 190 pagenum--; 191 displayPage(); 192 }); 193 nextButton = new JButton(Bundle.getMessage("ButtonNextPage")); 194 previewToolBar.add(nextButton); 195 nextButton.addActionListener((ActionEvent actionEvent) -> { 196 pagenum++; 197 displayPage(); 198 }); 199 pageCount = new JLabel(Bundle.getMessage("HeaderPageNum", pagenum, pageImages.size())); 200 pageCount.setBorder(new EmptyBorder(0, 10, 0, 10)); 201 previewToolBar.add(pageCount); 202 closeButton = new JButton(Bundle.getMessage("ButtonClose")); 203 previewToolBar.add(closeButton); 204 closeButton.addActionListener((ActionEvent actionEvent) -> { 205 if (page != null) { 206 page.dispose(); 207 } 208 previewFrame.dispose(); 209 }); 210 } 211 212 /** 213 * Display a page image in the preview pane. 214 * <p> 215 * Not part of the original HardcopyWriter class. 216 */ 217 protected void displayPage() { 218 // limit the pages to the actual range 219 if (pagenum > pageImages.size()) { 220 pagenum = pageImages.size(); 221 } 222 if (pagenum < 1) { 223 pagenum = 1; 224 } 225 // enable/disable the previous/next buttons as appropriate 226 previousButton.setEnabled(true); 227 nextButton.setEnabled(true); 228 if (pagenum == pageImages.size()) { 229 nextButton.setEnabled(false); 230 } 231 if (pagenum == 1) { 232 previousButton.setEnabled(false); 233 } 234 previewImage = pageImages.elementAt(pagenum - 1); 235 previewFrame.setVisible(false); 236 previewIcon.setImage(previewImage); 237 previewLabel.setIcon(previewIcon); 238 // put the label in the panel (already has a scroll pane) 239 previewPanel.add(previewLabel); 240 // set the page count info 241 pageCount.setText(Bundle.getMessage("HeaderPageNum", pagenum, pageImages.size())); 242 // repaint the frame but don't use pack() as we don't want resizing 243 previewFrame.invalidate(); 244 previewFrame.revalidate(); 245 previewFrame.setVisible(true); 246 } 247 248 /** 249 * Send text to Writer output. 250 * 251 * @param buffer block of text characters 252 * @param index position to start printing 253 * @param len length (number of characters) of output 254 */ 255 @Override 256 public void write(char[] buffer, int index, int len) { 257 synchronized (this.lock) { 258 // loop through all characters passed to us 259 line = ""; 260 for (int i = index; i < index + len; i++) { 261 // if we haven't begun a new page, do that now 262 if (page == null) { 263 newpage(); 264 } 265 266 // if the character is a line terminator, begin a new line 267 // unless its \n after \r 268 if (buffer[i] == '\n') { 269 if (!last_char_was_return) { 270 newline(); 271 } 272 continue; 273 } 274 if (buffer[i] == '\r') { 275 newline(); 276 last_char_was_return = true; 277 continue; 278 } else { 279 last_char_was_return = false; 280 } 281 282 if (buffer[i] == '\f') { 283 pageBreak(); 284 } 285 286 // if some other non-printing char, ignore it 287 if (Character.isWhitespace(buffer[i]) && !Character.isSpaceChar(buffer[i]) && (buffer[i] != '\t')) { 288 continue; 289 } 290 // if no more characters will fit on the line, start new line 291 if (charoffset >= width) { 292 newline(); 293 // also start a new page if needed 294 if (page == null) { 295 newpage(); 296 } 297 } 298 299 // now print the page 300 // if a space, skip one space 301 // if a tab, skip the necessary number 302 // otherwise print the character 303 // We need to position each character one-at-a-time to 304 // match the FontMetrics 305 if (buffer[i] == '\t') { 306 int tab = 8 - (charnum % 8); 307 charnum += tab; 308 charoffset = charnum * metrics.charWidth('m'); 309 for (int t = 0; t < tab; t++) { 310 line += " "; 311 } 312 } else { 313 line += buffer[i]; 314 charnum++; 315 charoffset += metrics.charWidth(buffer[i]); 316 } 317 } 318 if (page != null && pagenum >= prFirst) { 319 page.drawString(line, x0, y0 + (linenum * lineheight) + lineascent); 320 } 321 } 322 } 323 324 /** 325 * Write a given String with the desired color. 326 * <p> 327 * Reset the text color back to the default after the string is written. 328 * 329 * @param c the color desired for this String 330 * @param s the String 331 * @throws java.io.IOException if unable to write to printer 332 */ 333 public void write(Color c, String s) throws IOException { 334 charoffset = 0; 335 if (page == null) { 336 newpage(); 337 } 338 if (page != null) { 339 page.setColor(c); 340 } 341 write(s); 342 // note that the above write(s) can cause the page to become null! 343 if (page != null) { 344 page.setColor(color); // reset color 345 } 346 } 347 348 @Override 349 public void flush() { 350 } 351 352 /** 353 * Handle close event of pane. Modified to clean up the added preview 354 * capability. 355 */ 356 @Override 357 public void close() { 358 synchronized (this.lock) { 359 if (isPreview) { 360 // new JMRI code using try / catch declaration can call this close twice 361 // writer.close() is no longer needed. Work around next line. 362 if (!pageImages.contains(previewImage)) { 363 pageImages.addElement(previewImage); 364 } 365 // set up first page for display in preview frame 366 // to get the image displayed, put it in an icon and the icon in a label 367 pagenum = 1; 368 displayPage(); 369 } 370 if (page != null) { 371 page.dispose(); 372 } 373 if (job != null) { 374 job.end(); 375 } 376 } 377 } 378 379 /** 380 * Free up resources . 381 * <p> 382 * Added so that a preview can be canceled. 383 */ 384 public void dispose() { 385 synchronized (this.lock) { 386 if (page != null) { 387 page.dispose(); 388 } 389 previewFrame.dispose(); 390 if (job != null) { 391 job.end(); 392 } 393 } 394 } 395 396 public void setFontStyle(int style) { 397 synchronized (this.lock) { 398 // try to set a new font, but restore current one if it fails 399 Font current = font; 400 try { 401 font = new Font(fontName, style, fontsize); 402 fontStyle = style; 403 } catch (Exception e) { 404 font = current; 405 } 406 // if a page is pending, set the new font, else newpage() will 407 if (page != null) { 408 page.setFont(font); 409 } 410 } 411 } 412 413 public int getLineHeight() { 414 return this.lineheight; 415 } 416 417 public int getFontSize() { 418 return this.fontsize; 419 } 420 421 public int getCharWidth() { 422 return this.charwidth; 423 } 424 425 public int getLineAscent() { 426 return this.lineascent; 427 } 428 429 public void setFontName(String name) { 430 synchronized (this.lock) { 431 // try to set a new font, but restore current one if it fails 432 Font current = font; 433 try { 434 font = new Font(name, fontStyle, fontsize); 435 fontName = name; 436 metrics = frame.getFontMetrics(font); 437 lineheight = metrics.getHeight(); 438 lineascent = metrics.getAscent(); 439 charwidth = metrics.charWidth('m'); 440 441 // compute lines and columns within margins 442 chars_per_line = width / charwidth; 443 lines_per_page = height / lineheight; 444 } catch (RuntimeException e) { 445 font = current; 446 } 447 // if a page is pending, set the new font, else newpage() will 448 if (page != null) { 449 page.setFont(font); 450 } 451 } 452 } 453 454 /** 455 * sets the default text color 456 * 457 * @param c the new default text color 458 */ 459 public void setTextColor(Color c) { 460 color = c; 461 } 462 463 /** 464 * End the current page. Subsequent output will be on a new page 465 */ 466 public void pageBreak() { 467 synchronized (this.lock) { 468 if (isPreview) { 469 pageImages.addElement(previewImage); 470 } 471 if (page != null) { 472 page.dispose(); 473 } 474 page = null; 475 newpage(); 476 } 477 } 478 479 /** 480 * Return the number of columns of characters that fit on a page. 481 * 482 * @return the number of characters in a line 483 */ 484 public int getCharactersPerLine() { 485 return this.chars_per_line; 486 } 487 488 /** 489 * Return the number of lines that fit on a page. 490 * 491 * @return the number of lines in a page 492 */ 493 public int getLinesPerPage() { 494 return this.lines_per_page; 495 } 496 497 /** 498 * Internal method begins a new line method modified by Dennis Miller to add 499 * preview capability 500 */ 501 protected void newline() { 502 if (page != null && pagenum >= prFirst) { 503 page.drawString(line, x0, y0 + (linenum * lineheight) + lineascent); 504 } 505 line = ""; 506 charnum = 0; 507 charoffset = 0; 508 linenum++; 509 if (linenum >= lines_per_page) { 510 if (isPreview) { 511 pageImages.addElement(previewImage); 512 } 513 if (page != null) { 514 page.dispose(); 515 } 516 page = null; 517 newpage(); 518 } 519 } 520 521 /** 522 * Internal method beings a new page and prints the header method modified 523 * by Dennis Miller to add preview capability 524 */ 525 protected void newpage() { 526 pagenum++; 527 linenum = 0; 528 charnum = 0; 529 // get a page graphics or image graphics object depending on output destination 530 if (page == null) { 531 if (!isPreview) { 532 if (pagenum >= prFirst) { 533 page = job.getGraphics(); 534 } else { 535 // The job.getGraphics() method will return null if the number of pages requested is greater than 536 // the number the user selected. Since the code checks for a null page in many places, we need to 537 // create a "dummy" page for the pages the user has decided to skip. 538 JFrame f = new JFrame(); 539 f.pack(); 540 page = f.createImage(pagesize.width, pagesize.height).getGraphics(); 541 } 542 } else { // Preview 543 previewImage = previewPanel.createImage(pagesize.width, pagesize.height); 544 page = previewImage.getGraphics(); 545 page.setColor(Color.white); 546 page.fillRect(0, 0, previewImage.getWidth(previewPanel), previewImage.getHeight(previewPanel)); 547 page.setColor(color); 548 } 549 } 550 if (printHeader && page != null && pagenum >= prFirst) { 551 page.setFont(headerfont); 552 page.drawString(jobname, x0, headery); 553 554 String s = "- " + pagenum + " -"; // print page number centered 555 int w = headermetrics.stringWidth(s); 556 page.drawString(s, x0 + (this.width - w) / 2, headery); 557 w = headermetrics.stringWidth(time); 558 page.drawString(time, x0 + width - w, headery); 559 560 // draw a line under the header 561 int y = headery + headermetrics.getDescent() + 1; 562 page.drawLine(x0, y, x0 + width, y); 563 } 564 // set basic font 565 if (page != null) { 566 page.setFont(font); 567 } 568 } 569 570 /** 571 * Write a graphic to the printout. 572 * <p> 573 * This was not in the original class, but was added afterwards by Bob 574 * Jacobsen. Modified by D Miller. 575 * <p> 576 * The image is positioned on the right side of the paper, at the current 577 * height. 578 * 579 * @param c image to write 580 * @param i ignored, but maintained for API compatibility 581 */ 582 public void write(Image c, Component i) { 583 // if we haven't begun a new page, do that now 584 if (page == null) { 585 newpage(); 586 } 587 588 // D Miller: Scale the icon slightly smaller to make page layout easier and 589 // position one character to left of right margin 590 int x = x0 + width - (c.getWidth(null) * 2 / 3 + charwidth); 591 int y = y0 + (linenum * lineheight) + lineascent; 592 593 if (page != null && pagenum >= prFirst) { 594 page.drawImage(c, x, y, c.getWidth(null) * 2 / 3, c.getHeight(null) * 2 / 3, null); 595 } 596 } 597 598 /** 599 * Write a graphic to the printout. 600 * <p> 601 * This was not in the original class, but was added afterwards by Kevin 602 * Dickerson. it is a copy of the write, but without the scaling. 603 * <p> 604 * The image is positioned on the right side of the paper, at the current 605 * height. 606 * 607 * @param c the image to print 608 * @param i ignored but maintained for API compatibility 609 */ 610 public void writeNoScale(Image c, Component i) { 611 // if we haven't begun a new page, do that now 612 if (page == null) { 613 newpage(); 614 } 615 616 int x = x0 + width - (c.getWidth(null) + charwidth); 617 int y = y0 + (linenum * lineheight) + lineascent; 618 619 if (page != null && pagenum >= prFirst) { 620 page.drawImage(c, x, y, c.getWidth(null), c.getHeight(null), null); 621 } 622 } 623 624 /** 625 * A Method to allow a JWindow to print itself at the current line position 626 * <p> 627 * This was not in the original class, but was added afterwards by Dennis 628 * Miller. 629 * <p> 630 * Intended to allow for a graphic printout of the speed table, but can be 631 * used to print any window. The JWindow is passed to the method and prints 632 * itself at the current line and aligned at the left margin. The calling 633 * method should check for sufficient space left on the page and move it to 634 * the top of the next page if there isn't enough space. 635 * 636 * @param jW the window to print 637 */ 638 public void write(JWindow jW) { 639 // if we haven't begun a new page, do that now 640 if (page == null) { 641 newpage(); 642 } 643 if (page != null && pagenum >= prFirst) { 644 int x = x0; 645 int y = y0 + (linenum * lineheight); 646 // shift origin to current printing position 647 page.translate(x, y); 648 // Window must be visible to print 649 jW.setVisible(true); 650 // Have the window print itself 651 jW.printAll(page); 652 // Make it invisible again 653 jW.setVisible(false); 654 // Get rid of the window now that it's printed and put the origin back where it was 655 jW.dispose(); 656 page.translate(-x, -y); 657 } 658 } 659 660 /** 661 * Draw a line on the printout. 662 * <p> 663 * This was not in the original class, but was added afterwards by Dennis 664 * Miller. 665 * <p> 666 * colStart and colEnd represent the horizontal character positions. The 667 * lines actually start in the middle of the character position to make it 668 * easy to draw vertical lines and space them between printed characters. 669 * <p> 670 * rowStart and rowEnd represent the vertical character positions. 671 * Horizontal lines are drawn underneath the row (line) number. They are 672 * offset so they appear evenly spaced, although they don't take into 673 * account any space needed for descenders, so they look best with all caps 674 * text 675 * 676 * @param rowStart vertical starting position 677 * @param colStart horizontal starting position 678 * @param rowEnd vertical ending position 679 * @param colEnd horizontal ending position 680 */ 681 public void write(int rowStart, int colStart, int rowEnd, int colEnd) { 682 // if we haven't begun a new page, do that now 683 if (page == null) { 684 newpage(); 685 } 686 int xStart = x0 + (colStart - 1) * charwidth + charwidth / 2; 687 int xEnd = x0 + (colEnd - 1) * charwidth + charwidth / 2; 688 int yStart = y0 + rowStart * lineheight + (lineheight - lineascent) / 2; 689 int yEnd = y0 + rowEnd * lineheight + (lineheight - lineascent) / 2; 690 if (page != null && pagenum >= prFirst) { 691 page.drawLine(xStart, yStart, xEnd, yEnd); 692 } 693 } 694 695 /** 696 * Get the current linenumber. 697 * <p> 698 * This was not in the original class, but was added afterwards by Dennis 699 * Miller. 700 * 701 * @return the line number within the page 702 */ 703 public int getCurrentLineNumber() { 704 return this.linenum; 705 } 706 707 /** 708 * Print vertical borders on the current line at the left and right sides of 709 * the page at character positions 0 and chars_per_line + 1. Border lines 710 * are one text line in height 711 * <p> 712 * This was not in the original class, but was added afterwards by Dennis 713 * Miller. 714 */ 715 public void writeBorders() { 716 write(this.linenum, 0, this.linenum + 1, 0); 717 write(this.linenum, this.chars_per_line + 1, this.linenum + 1, this.chars_per_line + 1); 718 } 719 720 /** 721 * Increase line spacing by a percentage 722 * <p> 723 * This method should be invoked immediately after a new HardcopyWriter is 724 * created. 725 * <p> 726 * This method was added to improve appearance when printing tables 727 * <p> 728 * This was not in the original class, added afterwards by DaveDuchamp. 729 * 730 * @param percent percentage by which to increase line spacing 731 */ 732 public void increaseLineSpacing(int percent) { 733 int delta = (lineheight * percent) / 100; 734 lineheight = lineheight + delta; 735 lineascent = lineascent + delta; 736 lines_per_page = height / lineheight; 737 } 738 739 public static class PrintCanceledException extends Exception { 740 741 public PrintCanceledException(String msg) { 742 super(msg); 743 } 744 } 745 746 // private final static Logger log = LoggerFactory.getLogger(HardcopyWriter.class); 747}