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