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