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 the number of columns of characters that fit on a page. 485 * 486 * @return the number of characters in a line 487 */ 488 public int getCharactersPerLine() { 489 return this.chars_per_line; 490 } 491 492 /** 493 * Return the number of lines that fit on a page. 494 * 495 * @return the number of lines in a page 496 */ 497 public int getLinesPerPage() { 498 return this.lines_per_page; 499 } 500 501 /** 502 * Internal method begins a new line method modified by Dennis Miller to add 503 * preview capability 504 */ 505 protected void newline() { 506 if (page != null && pagenum >= prFirst) { 507 page.drawString(line, x0, y0 + (linenum * lineheight) + lineascent); 508 } 509 line = ""; 510 charnum = 0; 511 charoffset = 0; 512 linenum++; 513 if (linenum >= lines_per_page) { 514 if (isPreview) { 515 pageImages.addElement(previewImage); 516 } 517 if (page != null) { 518 page.dispose(); 519 } 520 page = null; 521 newpage(); 522 } 523 } 524 525 /** 526 * Internal method beings a new page and prints the header method modified 527 * by Dennis Miller to add preview capability 528 */ 529 protected void newpage() { 530 pagenum++; 531 linenum = 0; 532 charnum = 0; 533 // get a page graphics or image graphics object depending on output destination 534 if (page == null) { 535 if (!isPreview) { 536 if (pagenum >= prFirst) { 537 page = job.getGraphics(); 538 } else { 539 // The job.getGraphics() method will return null if the number of pages requested is greater than 540 // the number the user selected. Since the code checks for a null page in many places, we need to 541 // create a "dummy" page for the pages the user has decided to skip. 542 JFrame f = new JFrame(); 543 f.pack(); 544 page = f.createImage(pagesize.width, pagesize.height).getGraphics(); 545 } 546 } else { // Preview 547 previewImage = previewPanel.createImage(pagesize.width, pagesize.height); 548 page = previewImage.getGraphics(); 549 page.setColor(Color.white); 550 page.fillRect(0, 0, previewImage.getWidth(previewPanel), previewImage.getHeight(previewPanel)); 551 page.setColor(color); 552 } 553 } 554 if (printHeader && page != null && pagenum >= prFirst) { 555 page.setFont(headerfont); 556 page.drawString(jobname, x0, headery); 557 558 String s = "- " + pagenum + " -"; // print page number centered 559 int w = headermetrics.stringWidth(s); 560 page.drawString(s, x0 + (this.width - w) / 2, headery); 561 w = headermetrics.stringWidth(time); 562 page.drawString(time, x0 + width - w, headery); 563 564 // draw a line under the header 565 int y = headery + headermetrics.getDescent() + 1; 566 page.drawLine(x0, y, x0 + width, y); 567 } 568 // set basic font 569 if (page != null) { 570 page.setFont(font); 571 } 572 } 573 574 /** 575 * Write a graphic to the printout. 576 * <p> 577 * This was not in the original class, but was added afterwards by Bob 578 * Jacobsen. Modified by D Miller. 579 * <p> 580 * The image is positioned on the right side of the paper, at the current 581 * height. 582 * 583 * @param c image to write 584 * @param i ignored, but maintained for API compatibility 585 */ 586 public void write(Image c, Component i) { 587 // if we haven't begun a new page, do that now 588 if (page == null) { 589 newpage(); 590 } 591 592 // D Miller: Scale the icon slightly smaller to make page layout easier and 593 // position one character to left of right margin 594 int x = x0 + width - (c.getWidth(null) * 2 / 3 + charwidth); 595 int y = y0 + (linenum * lineheight) + lineascent; 596 597 if (page != null && pagenum >= prFirst) { 598 page.drawImage(c, x, y, c.getWidth(null) * 2 / 3, c.getHeight(null) * 2 / 3, null); 599 } 600 } 601 602 /** 603 * Write a graphic to the printout. 604 * <p> 605 * This was not in the original class, but was added afterwards by Kevin 606 * Dickerson. it is a copy of the write, but without the scaling. 607 * <p> 608 * The image is positioned on the right side of the paper, at the current 609 * height. 610 * 611 * @param c the image to print 612 * @param i ignored but maintained for API compatibility 613 */ 614 public void writeNoScale(Image c, Component i) { 615 // if we haven't begun a new page, do that now 616 if (page == null) { 617 newpage(); 618 } 619 620 int x = x0 + width - (c.getWidth(null) + charwidth); 621 int y = y0 + (linenum * lineheight) + lineascent; 622 623 if (page != null && pagenum >= prFirst) { 624 page.drawImage(c, x, y, c.getWidth(null), c.getHeight(null), null); 625 } 626 } 627 628 /** 629 * A Method to allow a JWindow to print itself at the current line position 630 * <p> 631 * This was not in the original class, but was added afterwards by Dennis 632 * Miller. 633 * <p> 634 * Intended to allow for a graphic printout of the speed table, but can be 635 * used to print any window. The JWindow is passed to the method and prints 636 * itself at the current line and aligned at the left margin. The calling 637 * method should check for sufficient space left on the page and move it to 638 * the top of the next page if there isn't enough space. 639 * 640 * @param jW the window to print 641 */ 642 public void write(JWindow jW) { 643 // if we haven't begun a new page, do that now 644 if (page == null) { 645 newpage(); 646 } 647 if (page != null && pagenum >= prFirst) { 648 int x = x0; 649 int y = y0 + (linenum * lineheight); 650 // shift origin to current printing position 651 page.translate(x, y); 652 // Window must be visible to print 653 jW.setVisible(true); 654 // Have the window print itself 655 jW.printAll(page); 656 // Make it invisible again 657 jW.setVisible(false); 658 // Get rid of the window now that it's printed and put the origin back where it was 659 jW.dispose(); 660 page.translate(-x, -y); 661 } 662 } 663 664 /** 665 * Draw a line on the printout. 666 * <p> 667 * This was not in the original class, but was added afterwards by Dennis 668 * Miller. 669 * <p> 670 * colStart and colEnd represent the horizontal character positions. The 671 * lines actually start in the middle of the character position to make it 672 * easy to draw vertical lines and space them between printed characters. 673 * <p> 674 * rowStart and rowEnd represent the vertical character positions. 675 * Horizontal lines are drawn underneath the row (line) number. They are 676 * offset so they appear evenly spaced, although they don't take into 677 * account any space needed for descenders, so they look best with all caps 678 * text 679 * 680 * @param rowStart vertical starting position 681 * @param colStart horizontal starting position 682 * @param rowEnd vertical ending position 683 * @param colEnd horizontal ending position 684 */ 685 public void write(int rowStart, int colStart, int rowEnd, int colEnd) { 686 // if we haven't begun a new page, do that now 687 if (page == null) { 688 newpage(); 689 } 690 int xStart = x0 + (colStart - 1) * charwidth + charwidth / 2; 691 int xEnd = x0 + (colEnd - 1) * charwidth + charwidth / 2; 692 int yStart = y0 + rowStart * lineheight + (lineheight - lineascent) / 2; 693 int yEnd = y0 + rowEnd * lineheight + (lineheight - lineascent) / 2; 694 if (page != null && pagenum >= prFirst) { 695 page.drawLine(xStart, yStart, xEnd, yEnd); 696 } 697 } 698 699 /** 700 * Get the current linenumber. 701 * <p> 702 * This was not in the original class, but was added afterwards by Dennis 703 * Miller. 704 * 705 * @return the line number within the page 706 */ 707 public int getCurrentLineNumber() { 708 return this.linenum; 709 } 710 711 /** 712 * Print vertical borders on the current line at the left and right sides of 713 * the page at character positions 0 and chars_per_line + 1. Border lines 714 * are one text line in height 715 * <p> 716 * This was not in the original class, but was added afterwards by Dennis 717 * Miller. 718 */ 719 public void writeBorders() { 720 write(this.linenum, 0, this.linenum + 1, 0); 721 write(this.linenum, this.chars_per_line + 1, this.linenum + 1, this.chars_per_line + 1); 722 } 723 724 /** 725 * Increase line spacing by a percentage 726 * <p> 727 * This method should be invoked immediately after a new HardcopyWriter is 728 * created. 729 * <p> 730 * This method was added to improve appearance when printing tables 731 * <p> 732 * This was not in the original class, added afterwards by DaveDuchamp. 733 * 734 * @param percent percentage by which to increase line spacing 735 */ 736 public void increaseLineSpacing(int percent) { 737 int delta = (lineheight * percent) / 100; 738 lineheight = lineheight + delta; 739 lineascent = lineascent + delta; 740 lines_per_page = height / lineheight; 741 } 742 743 public static class PrintCanceledException extends Exception { 744 745 public PrintCanceledException(String msg) { 746 super(msg); 747 } 748 } 749 750 // private final static Logger log = LoggerFactory.getLogger(HardcopyWriter.class); 751}