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}