/* * 11/14/2003 * * RPrintUtilities.java - A collection of static methods useful for printing * text from Swing text components. * Copyright (C) 2003 Robert Futrell * robert_futrell at users.sourceforge.net * http://fifesoft.com/rsyntaxtextarea * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. */ package org.fife.print; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.Segment; import javax.swing.text.JTextComponent; import javax.swing.text.TabExpander; import javax.swing.text.Utilities; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.print.*; /** * A collection of static methods useful for printing text from Swing text components. * * @author Robert Futrell * @version 1.0 */ public abstract class RPrintUtilities { private static int currentDocLineNumber; // The line number in the document we are currently on. private static int numDocLines; // The number of lines in the current document. private static Element rootElement; // The first Element (line) in the current document. // The characters at which to break a line if implementing word wrap. private static final char[] breakChars = { ' ', '\t', ',', '.', ';', '?', '!' }; // These variables are 'global' because RPrintTabExpander uses them. private static int xOffset; // The x-offset (for the page margin) when printing. private static int tabSizeInSpaces; // The length of a tab, in spaces. private static FontMetrics fm; // The metrics of the font currently being used to print. /** * Returns the position closest to, but before, position <code>maxCharsPerLine</code> in <code>line</code> of one of * the chars in <code>breakChars</code>, or simply returns <code>maxCharsPerLine-1</code> if none of the * <code>breakChars</code> comes before that position. This position represents the logical line break for this * <code>java.lang.String</code> if it is being printed in a monospaced font when lines can only be * <code>maxCharsPerLine</code> characters long. * * @param line * The text being printed. * @param maxCharsPerLine * Only up-to this many characters from <code>line</code> can be printed on one line. * @return The logical position at which to stop printing <code>line</code> to simulate word wrap. */ private static int getLineBreakPoint(String line, final int maxCharsPerLine) { int breakPoint = -1; for (int i = 0; i < breakChars.length; i++) { int breakCharPos = line.lastIndexOf(breakChars[i], maxCharsPerLine - 1); if (breakCharPos > breakPoint) breakPoint = breakCharPos; } return (breakPoint == -1 ? maxCharsPerLine - 1 : breakPoint); } /** * Prints a <code>Document</code> using a monospaced font, and does no word wrapping (ie, words will wrap mid-word * to the next line). This method is expected to be called from Printable 'print(Graphics g)' functions. * * @param g * The graphics context to write to. * @param doc * The <code>javax.swing.text.Document</code> to print. * @param fontSize * the point size to use for the monospaced font. * @param pageIndex * The page number to print. * @param pageFormat * The format to print the page with. * @param tabSize * The number of spaces to expand tabs to. * * @see #printDocumentMonospacedWordWrap */ public static int printDocumentMonospaced(Graphics g, Document doc, int fontSize, int pageIndex, PageFormat pageFormat, int tabSize) { g.setColor(Color.BLACK); g.setFont(new Font("Monospaced", Font.PLAIN, fontSize)); // Initialize our static variables (these are used by our tab expander below). tabSizeInSpaces = tabSize; fm = g.getFontMetrics(); // Create our tab expander. // RPrintTabExpander tabExpander = new RPrintTabExpander(); // Get width and height of characters in this monospaced font. int fontWidth = fm.charWidth('w'); // Any character will do as font is monospaced. int fontHeight = fm.getHeight(); int MAX_CHARS_PER_LINE = (int) pageFormat.getImageableWidth() / fontWidth; int MAX_LINES_PER_PAGE = (int) pageFormat.getImageableHeight() / fontHeight; final int STARTING_LINE_NUMBER = MAX_LINES_PER_PAGE * pageIndex; // The (x,y) coordinate to print at (in pixels, not characters). // Since y is the baseline of where we'll start printing (not the top-left // corner), we offset it by the font's ascent ( + 1 just for good measure). xOffset = (int) pageFormat.getImageableX(); int y = (int) pageFormat.getImageableY() + fm.getAscent() + 1; // A counter to keep track of the number of lines that WOULD HAVE been // printed if we were printing all lines. int numPrintedLines = 0; // Keep going while there are more lines in the document. currentDocLineNumber = 0; // The line number of the document we're currently on. rootElement = doc.getDefaultRootElement(); // To shorten accesses in our loop. numDocLines = rootElement.getElementCount(); // The number of lines in our document. while (currentDocLineNumber < numDocLines) { // Get the line we are going to print. String curLineString; Element currentLine = rootElement.getElement(currentDocLineNumber); int startOffs = currentLine.getStartOffset(); try { curLineString = doc.getText(startOffs, currentLine.getEndOffset() - startOffs); } catch (BadLocationException ble) { // Never happens ble.printStackTrace(); return Printable.NO_SUCH_PAGE; } // Get rid of newlines, because they end up as boxes if you don't; this is a monospaced font. curLineString = curLineString.replaceAll("\n", ""); // Replace tabs with how many spaces they should be. if (tabSizeInSpaces == 0) { curLineString = curLineString.replaceAll("\t", ""); } else { int tabIndex = curLineString.indexOf('\t'); while (tabIndex > -1) { int spacesNeeded = tabSizeInSpaces - (tabIndex % tabSizeInSpaces); String replacementString = ""; for (int i = 0; i < spacesNeeded; i++) replacementString += ' '; // Note that "\t" is actually a regex for this method. curLineString = curLineString.replaceFirst("\t", replacementString); tabIndex = curLineString.indexOf('\t'); } } // If this document line is too long to fit on one printed line on the page, // break it up into multpile lines. while (curLineString.length() > MAX_CHARS_PER_LINE) { numPrintedLines++; if (numPrintedLines > STARTING_LINE_NUMBER) { g.drawString(curLineString.substring(0, MAX_CHARS_PER_LINE), xOffset, y); y += fontHeight; if (numPrintedLines == STARTING_LINE_NUMBER + MAX_LINES_PER_PAGE) return Printable.PAGE_EXISTS; } curLineString = curLineString.substring(MAX_CHARS_PER_LINE, curLineString.length()); } currentDocLineNumber += 1; // We have printed one more line from the document. numPrintedLines++; if (numPrintedLines > STARTING_LINE_NUMBER) { g.drawString(curLineString, xOffset, y); y += fontHeight; if (numPrintedLines == STARTING_LINE_NUMBER + MAX_LINES_PER_PAGE) return Printable.PAGE_EXISTS; } } // Now, the whole document has been "printed." Decide if this page had any text on it or not. if (numPrintedLines > STARTING_LINE_NUMBER) return Printable.PAGE_EXISTS; return Printable.NO_SUCH_PAGE; } /** * Prints a <code>Document</code> using a monospaced font, word wrapping on the characters ' ', '\t', '\n', ',', * '.', and ';'. This method is expected to be called from Printable 'print(Graphics g)' functions. * * @param g * The graphics context to write to. * @param doc * The <code>javax.swing.text.Document</code> to print. * @param fontSize * the point size to use for the monospaced font. * @param pageIndex * The page number to print. * @param pageFormat * The format to print the page with. * @param tabSize * The number of spaces to expand tabs to. * * @see #printDocumentMonospaced */ public static int printDocumentMonospacedWordWrap(Graphics g, Document doc, int fontSize, int pageIndex, PageFormat pageFormat, int tabSize) { g.setColor(Color.BLACK); g.setFont(new Font("Monospaced", Font.PLAIN, fontSize)); // Initialize our static variables (these are used by our tab expander below). tabSizeInSpaces = tabSize; fm = g.getFontMetrics(); // Create our tab expander. // RPrintTabExpander tabExpander = new RPrintTabExpander(); // Get width and height of characters in this monospaced font. int fontWidth = fm.charWidth('w'); // Any character will do here, since font is monospaced. int fontHeight = fm.getHeight(); int MAX_CHARS_PER_LINE = (int) pageFormat.getImageableWidth() / fontWidth; int MAX_LINES_PER_PAGE = (int) pageFormat.getImageableHeight() / fontHeight; final int STARTING_LINE_NUMBER = MAX_LINES_PER_PAGE * pageIndex; // The (x,y) coordinate to print at (in pixels, not characters). // Since y is the baseline of where we'll start printing (not the top-left // corner), we offset it by the font's ascent ( + 1 just for good measure). xOffset = (int) pageFormat.getImageableX(); int y = (int) pageFormat.getImageableY() + fm.getAscent() + 1; // A counter to keep track of the number of lines that WOULD HAVE been // printed if we were printing all lines. int numPrintedLines = 0; // Keep going while there are more lines in the document. currentDocLineNumber = 0; // The line number of the document we're currently on. rootElement = doc.getDefaultRootElement(); // To shorten accesses in our loop. numDocLines = rootElement.getElementCount(); // The number of lines in our document. while (currentDocLineNumber < numDocLines) { // Get the line we are going to print. String curLineString; Element currentLine = rootElement.getElement(currentDocLineNumber); int startOffs = currentLine.getStartOffset(); try { curLineString = doc.getText(startOffs, currentLine.getEndOffset() - startOffs); } catch (BadLocationException ble) { // Never happens ble.printStackTrace(); return Printable.NO_SUCH_PAGE; } // Remove newlines, because they end up as boxes if you don't; this is a monospaced font. curLineString = curLineString.replaceAll("\n", ""); // Replace tabs with how many spaces they should be. if (tabSizeInSpaces == 0) { curLineString = curLineString.replaceAll("\t", ""); } else { int tabIndex = curLineString.indexOf('\t'); while (tabIndex > -1) { int spacesNeeded = tabSizeInSpaces - (tabIndex % tabSizeInSpaces); String replacementString = ""; for (int i = 0; i < spacesNeeded; i++) replacementString += ' '; // Note that "\t" is actually a regex for this method. curLineString = curLineString.replaceFirst("\t", replacementString); tabIndex = curLineString.indexOf('\t'); } } // If this document line is too long to fit on one printed line on the page, // break it up into multpile lines. while (curLineString.length() > MAX_CHARS_PER_LINE) { int breakPoint = getLineBreakPoint(curLineString, MAX_CHARS_PER_LINE) + 1; numPrintedLines++; if (numPrintedLines > STARTING_LINE_NUMBER) { g.drawString(curLineString.substring(0, breakPoint), xOffset, y); y += fontHeight; if (numPrintedLines == STARTING_LINE_NUMBER + MAX_LINES_PER_PAGE) return Printable.PAGE_EXISTS; } curLineString = curLineString.substring(breakPoint, curLineString.length()); } currentDocLineNumber += 1; // We have printed one more line from the document. numPrintedLines++; if (numPrintedLines > STARTING_LINE_NUMBER) { g.drawString(curLineString, xOffset, y); y += fontHeight; if (numPrintedLines == STARTING_LINE_NUMBER + MAX_LINES_PER_PAGE) return Printable.PAGE_EXISTS; } } // Now, the whole document has been "printed." Decide if this page had any text on it or not. if (numPrintedLines > STARTING_LINE_NUMBER) return Printable.PAGE_EXISTS; return Printable.NO_SUCH_PAGE; } /** * Prints a <code>Document</code> using the specified font, word wrapping on the characters ' ', '\t', '\n', ',', * '.', and ';'. This method is expected to be called from Printable 'print(Graphics g)' functions. * * @param g * The graphics context to write to. * @param textComponent * The <code>javax.swing.text.JTextComponent</code> whose text you're printing. * @param font * The font to use for printing. If <code>null</code>, then <code>textComponent</code>'s font is used. * @param pageIndex * The page number to print. * @param pageFormat * The format to print the page with. * @param tabSize * The number of spaces to convert tabs to. * */ public static int printDocumentWordWrap(Graphics g, JTextComponent textComponent, Font font, int pageIndex, PageFormat pageFormat, int tabSize) { // Initialize our graphics object. g.setColor(Color.BLACK); g.setFont(font != null ? font : textComponent.getFont()); // Initialize our static variables (these are used by our tab expander below). tabSizeInSpaces = tabSize; fm = g.getFontMetrics(); int fontHeight = fm.getHeight(); final int LINE_LENGTH_IN_PIXELS = (int) pageFormat.getImageableWidth(); final int MAX_LINES_PER_PAGE = (int) pageFormat.getImageableHeight() / fontHeight; final int STARTING_LINE_NUMBER = MAX_LINES_PER_PAGE * pageIndex; // Create our tab expander. RPrintTabExpander tabExpander = new RPrintTabExpander(); // The (x,y) coordinate to print at (in pixels, not characters). // Since y is the baseline of where we'll start printing (not the top-left // corner), we offset it by the font's ascent ( + 1 just for good measure). xOffset = (int) pageFormat.getImageableX(); int y = (int) pageFormat.getImageableY() + fm.getAscent() + 1; // A counter to keep track of the number of lines that WOULD HAVE been // printed if we were printing all lines. int numPrintedLines = 0; // Keep going while there are more lines in the document. Document doc = textComponent.getDocument(); rootElement = doc.getDefaultRootElement(); numDocLines = rootElement.getElementCount(); // The number of lines in our document. currentDocLineNumber = 0; // The line number of the document we're currently on. int startingOffset = 0; // Used when a line is so long it has to be wrapped. while (currentDocLineNumber < numDocLines) { Segment currentLineSeg = new Segment(); // Get the current line (as an Element), and its starting and ending offset in doc. Element currentLine = rootElement.getElement(currentDocLineNumber); int currentLineStart = currentLine.getStartOffset(); int currentLineEnd = currentLine.getEndOffset(); // Put the chars of this line in currentLineSeg, but only starting at our desired offset // (because this line may be the second part of a wrapped line, so we'd start after the part // that has already been printed). try { doc.getText(currentLineStart + startingOffset, currentLineEnd - (currentLineStart + startingOffset), currentLineSeg); } catch (BadLocationException ble) { System.err.println("BadLocationException in print (where there shouldn't be one!): " + ble); return Printable.NO_SUCH_PAGE; } // Remove any spaces and/or tabs from the end of the segment (would cause problems if you left 'em). currentLineSeg = removeEndingWhitespace(currentLineSeg); // Figger out how long the line is, in pixels. int currentLineLengthInPixels = Utilities.getTabbedTextWidth(currentLineSeg, fm, 0, tabExpander, 0); // System.err.println("'" + currentLineSeg + "' - " + currentLineLengthInPixels + "/" + // LINE_LENGTH_IN_PIXELS); // If it'll fit by itself on a printed line, great. if (currentLineLengthInPixels <= LINE_LENGTH_IN_PIXELS) { currentDocLineNumber += 1; // We (will) have printed one more line from the document. startingOffset = 0; // Start at the first character in the new document line. } // If it won't fit on a printed line by itself (i.e., it needs to be wrapped)... else { // Loop while the current line is too long to fit on a printed line. int currentPos = -1; while (currentLineLengthInPixels > LINE_LENGTH_IN_PIXELS) { // System.err.println("'" + currentLineSeg + "' - " + currentLineLengthInPixels + "/" + // LINE_LENGTH_IN_PIXELS); // Remove any spaces and/or tabs from the end of the segment (would cause problems if you left 'em). currentLineSeg = removeEndingWhitespace(currentLineSeg); // currentPos will be the last position in the current text of a "line break character." currentPos = -1; String currentLineString = currentLineSeg.toString(); for (int i = 0; i < breakChars.length; i++) { // "+1" below so we include the character on the line. int pos = currentLineString.lastIndexOf(breakChars[i]) + 1; // if (pos>-1 && pos>currentPos) // currentPos = pos; if (pos > 0 && pos > currentPos & pos != currentLineString.length()) currentPos = pos; } // If we didn't find a line break character, we'll simply break the line at // the last character that fits on a printed line. // So here, we set currentPos to be the position of the last character that fits // on the current printed line. if (currentPos == -1) { // Fix currentLineSeg so that it contains exactly enough text to fit in // LINE_LENGTH_IN_PIXELS pixels... currentPos = 0; do { currentPos++; try { doc.getText(currentLineStart + startingOffset, currentPos, currentLineSeg); } catch (BadLocationException ble) { System.err.println(ble); return Printable.NO_SUCH_PAGE; } currentLineLengthInPixels = Utilities. getTabbedTextWidth(currentLineSeg, fm, 0, tabExpander, 0); } while (currentLineLengthInPixels <= LINE_LENGTH_IN_PIXELS); currentPos--; } try { doc.getText((currentLineStart + startingOffset), currentPos, currentLineSeg); } catch (BadLocationException ble) { System.err.println("BadLocationException in print (a):"); System.err.println("==> currentLineStart: " + currentLineStart + "; startingOffset: " + startingOffset + "; currentPos: " + currentPos); System.err.println("==> Range: " + (currentLineStart + startingOffset) + " - " + (currentLineStart + startingOffset + currentPos)); ble.printStackTrace(); return Printable.NO_SUCH_PAGE; } currentLineLengthInPixels = Utilities.getTabbedTextWidth(currentLineSeg, fm, 0, tabExpander, 0); } // End of while (currentLineLengthInPixels > LINE_LENGTH_IN_PIXELS). startingOffset += currentPos; // Where to start (offset from line's start), since this line wraps. } // End of else. numPrintedLines++; if (numPrintedLines > STARTING_LINE_NUMBER) { // g.drawString(currentLineSeg.toString(), xOffset,y); Utilities.drawTabbedText(currentLineSeg, xOffset, y, g, tabExpander, 0); y += fontHeight; if (numPrintedLines == STARTING_LINE_NUMBER + MAX_LINES_PER_PAGE) return Printable.PAGE_EXISTS; } } // Now, the whole document has been "printed." Decide if this page had any text on it or not. if (numPrintedLines > STARTING_LINE_NUMBER) return Printable.PAGE_EXISTS; return Printable.NO_SUCH_PAGE; } /** * Removes any spaces or tabs from the end of the segment. * * @param segment * The segment from which to remove tailing whitespace. * @return <code>segment</code> with trailing whitespace removed. */ private static Segment removeEndingWhitespace(Segment segment) { int toTrim = 0; char currentChar = segment.setIndex(segment.getEndIndex() - 1); while ((currentChar == ' ' || currentChar == '\t') && currentChar != Segment.DONE) { toTrim++; currentChar = segment.previous(); } String stringVal = segment.toString(); String newStringVal = stringVal.substring(0, stringVal.length() - toTrim); return new Segment(newStringVal.toCharArray(), 0, newStringVal.length()); } /** * A tab expander for the document currently being printed with the font being used for the printing. */ private static class RPrintTabExpander implements TabExpander { RPrintTabExpander() { } public float nextTabStop(float x, int tabOffset) { if (tabSizeInSpaces == 0) return x; int tabSizeInPixels = tabSizeInSpaces * fm.charWidth(' '); int ntabs = (((int) x) - xOffset) / tabSizeInPixels; return xOffset + ((ntabs + 1) * tabSizeInPixels); } } }