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