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