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}