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