/*
* BufferPrintable.java - Printable implementation
* :tabSize=4:indentSize=4:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 2016 Dale Anson
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.gjt.sp.jedit.print;
//{{{ Imports
import javax.swing.text.TabExpander;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.print.*;
import java.awt.*;
import static java.awt.RenderingHints.*;
import java.util.*;
import java.util.List;
import javax.print.attribute.standard.Chromaticity;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.PageRanges;
import org.gjt.sp.jedit.syntax.*;
import org.gjt.sp.jedit.*;
import org.gjt.sp.util.*;
/**
* A new buffer printable that does a lot more than the old one, like properly
* printing ranges of pages, reverse page printing, printing just a selection,
* and so on.
* @version $Id: BufferPrintable.java 24442 2016-06-29 23:29:25Z daleanson $
*/
class BufferPrintable1_7 implements Printable
{
private static Color headerColor = Color.lightGray;
private static Color headerTextColor = Color.black;
private static Color footerColor = Color.lightGray;
private static Color footerTextColor = Color.black;
private static Color lineNumberColor = Color.gray;
private static Color textColor = Color.black;
private PrintRequestAttributeSet attributes;
private boolean firstCall;
private View view;
private Buffer buffer;
private boolean selection;
private int[] selectedLines;
private boolean reverse;
private int printRangeType = PrinterDialog.ALL;
private Font font;
private SyntaxStyle[] styles;
private boolean header;
private boolean footer;
private boolean lineNumbers;
private HashMap<Integer, Range> pages = null;
private int currentPhysicalLine;
private LineMetrics lm;
private final List<Chunk> lineList;
private FontRenderContext frc;
private DisplayTokenHandler tokenHandler;
BufferPrintable1_7(PrintRequestAttributeSet attributes, View view, Buffer buffer)
{
this.attributes = attributes;
this.view = view;
this.buffer = buffer;
firstCall = true; // pages and page ranges are calculated only once
header = jEdit.getBooleanProperty("print.header");
footer = jEdit.getBooleanProperty("print.footer");
lineNumbers = jEdit.getBooleanProperty("print.lineNumbers");
font = jEdit.getFontProperty("print.font");
boolean color = Chromaticity.COLOR.equals(attributes.get(Chromaticity.class));
styles = org.gjt.sp.util.SyntaxUtilities.loadStyles(jEdit.getProperty("print.font"), jEdit.getIntegerProperty("print.fontsize", 10), color);
styles[Token.NULL] = new SyntaxStyle(textColor, null, font);
// assume the paper is white, so change any white text to black
for(int i = 0; i < styles.length; i++)
{
SyntaxStyle s = styles[i];
if(s.getForegroundColor().equals(Color.WHITE) && s.getBackgroundColor() == null)
{
styles[i] = new SyntaxStyle(Color.BLACK, s.getBackgroundColor(), s.getFont());
}
}
lineList = new ArrayList<Chunk>();
tokenHandler = new DisplayTokenHandler();
}
public void setFont(Font font)
{
if (font != null)
{
this.font = font;
}
}
/**
* Set the line numbers that are selected in the text area.
* @param lines An array of lines that are selected in the text area.
*/
public void setSelectedLines(int[] lines)
{
selectedLines = Arrays.copyOf(lines, lines.length);
Arrays.sort(selectedLines);
}
/**
* Set to <code>true</code> to print the pages in reverse order, that is, print
* the last page first and the first page last.
* @param b Whether to print in reverse or not.
*/
public void setReverse(boolean b)
{
reverse = b;
}
/**
* Set the print range type.
* @param printRangeType One of PrinterDialog.ALL, RANGE, CURRENT_PAGE, or SELECTION.
*/
public void setPrintRangeType(int printRangeType)
{
this.printRangeType = printRangeType;
selection = PrinterDialog.SELECTION == printRangeType;
}
// useful to avoid having to recalculate the page ranges if they are already known
public void setPages(HashMap<Integer, Range> pages)
{
this.pages = pages;
}
// this can be called multiple times by the print system for the same page, and
// all calls must be handled for the page to print properly.
public int print(Graphics _gfx, PageFormat pageFormat, int pageIndex) throws PrinterException
{
pageIndex += 1; // pageIndex is 0-based, but pages are 1-based
//Log.log(Log.DEBUG, this, "Asked to print page " + pageIndex);
if (firstCall && pages == null)
{
pages = calculatePages(_gfx, pageFormat);
if (pages == null || pages.isEmpty())
{
throw new PrinterException("Unable to determine page ranges.");
}
firstCall = false;
}
// figure out the current page if that is what is requested. I'm using
// the page that contains the caret as the current page.
// QUESTION: use the text area first physical line instead?
if (printRangeType == PrinterDialog.CURRENT_PAGE)
{
int caretLine = view.getTextArea().getCaretLine();
for (Integer i : pages.keySet())
{
Range range = pages.get(i);
if (range.contains(caretLine))
{
pageIndex = i;
break;
}
}
}
// adjust the page index for reverse printing
if (reverse && printRangeType != PrinterDialog.CURRENT_PAGE)
{
pageIndex = pages.size() - 1 - pageIndex;
//Log.log(Log.DEBUG, this, "Reverse is on, changing page index to " + pageIndex);
}
// go ahead and print the page
Range range = pages.get(pageIndex);
if ( (range == null || !inRange(pageIndex)) && printRangeType != PrinterDialog.CURRENT_PAGE )
{
//Log.log(Log.DEBUG, this, "Returning NO_SUCH_PAGE for page " + pageIndex);
return NO_SUCH_PAGE;
}
else {
printPage(_gfx, pageFormat, pageIndex, true);
}
//Log.log(Log.DEBUG, this, "Returning PAGE_EXISTS for page " + pageIndex);
return PAGE_EXISTS;
}
/**
* Parses the file to determine what lines belong to which page.
* @param _gfx The graphics context to use for the calculations.
* @param pageFormat The page format to use for the calculations.
* @param force If true, force the calculation regardless of large file and long
* line limits.
* @return A hashmap of page number = line range for that page
*
* NOTE: This handles large files and long lines poorly, if the buffer size
* is larger than the "largeBufferSize" property, a printer exception is thrown.
* Same for long lines, if any line in the buffer is longer than the
* "longLineLimit" property, a printer exception is thrown. This seems to be
* an adequate way to handle the large files, but is probably a poor way to
* handle files that aren't particularly large but only have one line.
*/
protected HashMap<Integer, Range> calculatePages(Graphics _gfx, PageFormat pageFormat) throws PrinterException
{
//Log.log(Log.DEBUG, this, "calculatePages for " + buffer.getName());
//Log.log(Log.DEBUG, this, "graphics.getClip = " + _gfx.getClip());
pages = new HashMap<Integer, Range>();
// check large file settings
String largeFileMode = buffer.getStringProperty("largefilemode");
if (!"full".equals(largeFileMode))
{
int largeBufferSize = jEdit.getIntegerProperty("largeBufferSize", 4000000);
if (buffer.getLength() > largeBufferSize)
{
throw new PrinterException("Buffer is too large to print.");
}
}
// ensure graphics and font rendering context are valid
if (_gfx == null)
{
// this can happen on startup when the graphics is not yet valid
return pages;
}
// use the rendering hints set in the text area option pane. The affine
// transform is basic and seems to work well in all cases. I've found
// that it's necessary to turn on the print spacing workaround in the
// text area option pane to not get character overlap. I think this
// setting should be on by default since it causes graphics.drawGlyphVector
// to be used to draw the characters and the javadoc says, "This is the
// fastest way to render a set of characters to the screen."
Graphics2D gfx = (Graphics2D)_gfx;
gfx.setRenderingHint(KEY_TEXT_ANTIALIASING, view.getTextArea().getPainter().getAntiAlias().renderHint());
boolean useFractionalFontMetrics = jEdit.getBooleanProperty("view.fracFontMetrics");
gfx.setRenderingHint(KEY_FRACTIONALMETRICS, (useFractionalFontMetrics ? VALUE_FRACTIONALMETRICS_ON : VALUE_FRACTIONALMETRICS_OFF));
gfx.setFont(font);
gfx.setTransform(new AffineTransform(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f));
frc = gfx.getFontRenderContext();
//Log.log(Log.DEBUG, this, "Font render context is " + frc);
// maximum printable area
double pageX = pageFormat.getImageableX();
double pageY = pageFormat.getImageableY();
double pageWidth = pageFormat.getImageableWidth();
double pageHeight = pageFormat.getImageableHeight();
//Log.log(Log.DEBUG, this, "calculatePages, total imageable: x=" + pageX + ", y=" + pageY + ", w=" + pageWidth + ", h=" + pageHeight);
// calculate header height
if(header)
{
double headerHeight = paintHeader(gfx, pageX, pageY, pageWidth, false);
pageY += headerHeight;
pageHeight -= headerHeight;
//Log.log(Log.DEBUG, this, "calculatePages, w/header imageable: x=" + pageX + ", y=" + pageY + ", w=" + pageWidth + ", h=" + pageHeight);
}
// calculate footer height
if(footer)
{
double footerHeight = paintFooter(gfx, pageX, pageY, pageWidth, pageHeight, 0, false);
pageHeight -= footerHeight;
//Log.log(Log.DEBUG, this, "calculatePages, w/footer imageable: x=" + pageX + ", y=" + pageY + ", w=" + pageWidth + ", h=" + pageHeight);
}
double lineNumberWidth = 0.0;
// determine line number width
if(lineNumbers)
{
String lineNumberDigits = String.valueOf(buffer.getLineCount());
StringBuilder digits = new StringBuilder();
for (int i = 0; i < lineNumberDigits.length(); i++)
{
digits.append('0');
}
lineNumberWidth = font.getStringBounds(digits.toString(), frc).getWidth();
}
// calculate tab size
int tabSize = jEdit.getIntegerProperty("print.tabSize", 4);
StringBuilder tabs = new StringBuilder();
char[] chars = new char[tabSize];
for(int i = 0; i < tabSize; i++)
{
tabs.append(' ');
}
double tabWidth = font.getStringBounds(tabs.toString(), frc).getWidth();
PrintTabExpander tabExpander = new PrintTabExpander(tabWidth);
// prep for calculations
lm = font.getLineMetrics("gGyYX", frc);
float lineHeight = lm.getHeight();
boolean printFolds = jEdit.getBooleanProperty("print.folds", true);
currentPhysicalLine = 0;
int pageCount = 1;
int startLine = 0;
double y = 0.0;
// measure each line
int longLineLimit = jEdit.getIntegerProperty("longLineLimit", 4000);
int bufferLineCount = buffer.getLineCount();
while (currentPhysicalLine <= bufferLineCount)
{
// line might be too long. The default long line limit is 4000 characters,
// which is about the number of characters that fit on a single letter
// size page, roughly 80 characters wide and 50 lines tall.
if (currentPhysicalLine < bufferLineCount && buffer.getLineLength(currentPhysicalLine) > longLineLimit)
{
throw new PrinterException("Line " + (currentPhysicalLine + 1) + " is too long to print.");
}
if (currentPhysicalLine == bufferLineCount)
{
// last page
Range range = new Range(startLine, currentPhysicalLine);
pages.put(new Integer(pageCount), range);
Log.log(Log.DEBUG, this, "calculatePages, page " + pageCount + " has " + range);
break;
}
// skip folded lines
if (!printFolds && !view.getTextArea().getDisplayManager().isLineVisible(currentPhysicalLine))
{
++ currentPhysicalLine;
continue;
}
// fill the line list
lineList.clear();
tokenHandler.init(styles, frc, tabExpander, lineList, (float)(pageWidth - lineNumberWidth), 0);
buffer.markTokens(currentPhysicalLine, tokenHandler);
// check that these lines will fit on the page
if(y + (lineHeight * (lineList.isEmpty() ? 1 : lineList.size())) > pageHeight)
{
Range range = new Range(startLine, Math.max(0, currentPhysicalLine - 1));
pages.put(new Integer(pageCount), range);
Log.log(Log.DEBUG, this, "calculatePages, page " + pageCount + " has " + range);
++ pageCount;
startLine = currentPhysicalLine;
y = 0.0;
continue;
}
for (int i = 0; i < (lineList.isEmpty() ? 1 : lineList.size()); i++)
{
y += lineHeight;
}
++ currentPhysicalLine;
}
return pages;
}
// returns true if the given page number is one of the pages requested to
// be printed
private boolean inRange(int pageNumber)
{
PageRanges ranges = (PageRanges)attributes.get(PageRanges.class);
boolean answer = false;
if (ranges == null)
{
answer = true;
}
else
{
answer = ranges.contains(pageNumber);
}
return answer;
}
// actually print the page to the graphics context
// pageIndex is 1-based
private void printPage(Graphics _gfx, PageFormat pageFormat, int pageIndex, boolean actuallyPaint)
{
//Log.log(Log.DEBUG, this, "printPage(" + pageIndex + ", " + actuallyPaint + ')');
Graphics2D gfx = (Graphics2D)_gfx;
float zoomLevel = 1.0f;
if (pageFormat instanceof PrintPreviewModel)
{
PrintPreviewModel model = (PrintPreviewModel)pageFormat;
zoomLevel = model.getZoomLevel();
font = font.deriveFont(font.getSize() * zoomLevel);
gfx.setRenderingHint(KEY_TEXT_ANTIALIASING, view.getTextArea().getPainter().getAntiAlias().renderHint());
boolean useFractionalFontMetrics = jEdit.getBooleanProperty("view.fracFontMetrics");
gfx.setRenderingHint(KEY_FRACTIONALMETRICS, (useFractionalFontMetrics ? VALUE_FRACTIONALMETRICS_ON : VALUE_FRACTIONALMETRICS_OFF));
gfx.setTransform(new AffineTransform(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f));
for(int i = 0; i < styles.length; i++)
{
SyntaxStyle s = styles[i];
styles[i] = new SyntaxStyle(s.getForegroundColor(), s.getBackgroundColor(), font);
}
}
gfx.setFont(font);
if (frc == null)
{
frc = gfx.getFontRenderContext();
}
// printable dimensions
double pageX = pageFormat.getImageableX();
double pageY = pageFormat.getImageableY();
double pageWidth = pageFormat.getImageableWidth();
double pageHeight = pageFormat.getImageableHeight();
//Log.log(Log.DEBUG, this, "#1 - Page dimensions: (" + pageX + ", " + pageY + ") " + pageWidth + 'x' + pageHeight);
// print header
if(header)
{
double headerHeight = paintHeader(gfx, pageX, pageY, pageWidth, actuallyPaint);
pageY += headerHeight;
pageHeight -= headerHeight;
}
// print footer
if(footer)
{
double footerHeight = paintFooter(gfx, pageX, pageY, pageWidth, pageHeight, pageIndex, actuallyPaint);
pageHeight -= footerHeight;
}
// determine line number width
double lineNumberWidth = 0.0;
if(lineNumbers)
{
String lineNumberDigits = String.valueOf(buffer.getLineCount());
StringBuilder digits = new StringBuilder();
for (int i = 0; i < lineNumberDigits.length(); i++)
{
digits.append('0');
}
lineNumberWidth = font.getStringBounds(digits.toString(), frc).getWidth();
}
//Log.log(Log.DEBUG,this,"#2 - Page dimensions: " + (pageWidth - lineNumberWidth) + 'x' + pageHeight);
// calculate tab size
int tabSize = jEdit.getIntegerProperty("print.tabSize", 4);
StringBuilder tabs = new StringBuilder();
for(int i = 0; i < tabSize; i++)
{
tabs.append(' ');
}
double tabWidth = font.getStringBounds(tabs.toString(), frc).getWidth();
PrintTabExpander tabExpander = new PrintTabExpander(tabWidth);
// prep for printing lines
lm = font.getLineMetrics("gGyYX", frc);
float lineHeight = lm.getHeight();
//Log.log(Log.DEBUG, this, "Line height is " + lineHeight);
double y = 0.0;
Range range = pages.get(pageIndex);
//Log.log(Log.DEBUG, this, "printing range for page " + pageIndex + ": " + range);
// print each line
for (currentPhysicalLine = range.getStart(); currentPhysicalLine <= range.getEnd(); currentPhysicalLine++)
{
if(currentPhysicalLine == buffer.getLineCount())
{
//Log.log(Log.DEBUG, this, "Finished buffer");
//Log.log(Log.DEBUG, this, "The end");
break;
}
if (!jEdit.getBooleanProperty("print.folds",true) && !view.getTextArea().getDisplayManager().isLineVisible(currentPhysicalLine))
{
//Log.log(Log.DEBUG, this, "Skipping invisible line");
continue;
}
// print only selected lines if printing selection
if (selection && Arrays.binarySearch(selectedLines, currentPhysicalLine) < 0)
{
//Log.log(Log.DEBUG, this, "Skipping non-selected line: " + currentPhysicalLine);
continue;
}
// fill the line list
lineList.clear();
tokenHandler.init(styles, frc, tabExpander, lineList, (float)(pageWidth - lineNumberWidth), -1);
buffer.markTokens(currentPhysicalLine, tokenHandler);
if(lineNumbers && actuallyPaint)
{
gfx.setFont(font);
gfx.setColor(lineNumberColor);
gfx.drawString(String.valueOf(currentPhysicalLine + 1), (float)pageX, (float)(pageY + y + lineHeight));
}
if (lineList.isEmpty())
{
// handle blank line
y += lineHeight;
}
else
{
for (Chunk chunk : lineList)
{
y += lineHeight;
Chunk chunks = chunk;
if (chunks != null && actuallyPaint)
{
Chunk.paintChunkBackgrounds(chunks, gfx, (float) (pageX + lineNumberWidth), (float) (pageY + y), lineHeight);
Chunk.paintChunkList(chunks, gfx, (float) (pageX + lineNumberWidth), (float) (pageY + y), true);
}
}
}
if (currentPhysicalLine == range.getEnd())
{
//Log.log(Log.DEBUG,this,"Finished page");
break;
}
}
}
private double paintHeader(Graphics2D gfx, double pageX, double pageY, double pageWidth, boolean actuallyPaint)
{
String headerText = jEdit.getProperty("print.headerText", new String[] { buffer.getName() });
FontRenderContext frc = gfx.getFontRenderContext();
lm = font.getLineMetrics(headerText, frc);
Rectangle2D bounds = font.getStringBounds(headerText, frc);
Rectangle2D headerBounds = new Rectangle2D.Double(pageX, pageY, pageWidth, bounds.getHeight());
if(actuallyPaint)
{
gfx.setColor(headerColor);
gfx.fill(headerBounds);
gfx.setColor(headerTextColor);
gfx.drawString(headerText, (float)(pageX + (pageWidth - bounds.getWidth()) / 2), (float)(pageY + lm.getAscent()));
}
return headerBounds.getHeight();
}
private double paintFooter(Graphics2D gfx, double pageX, double pageY, double pageWidth, double pageHeight, int pageIndex, boolean actuallyPaint)
{
String footerText = jEdit.getProperty("print.footerText", new Object[] { new Date(), Integer.valueOf(pageIndex)});
FontRenderContext frc = gfx.getFontRenderContext();
lm = font.getLineMetrics(footerText, frc);
Rectangle2D bounds = font.getStringBounds(footerText, frc);
Rectangle2D footerBounds = new Rectangle2D.Double(pageX, pageY + pageHeight - bounds.getHeight(), pageWidth, bounds.getHeight());
if(actuallyPaint)
{
gfx.setColor(footerColor);
gfx.fill(footerBounds);
gfx.setColor(footerTextColor);
gfx.drawString(footerText, (float)(pageX + (pageWidth - bounds.getWidth()) / 2), (float)(pageY + pageHeight - bounds.getHeight() + lm.getAscent()));
}
return footerBounds.getHeight();
}
static class PrintTabExpander implements TabExpander
{
private double tabWidth;
PrintTabExpander(double tabWidth)
{
this.tabWidth = tabWidth;
}
public float nextTabStop(float x, int tabOffset)
{
int ntabs = (int)((x + 1) / tabWidth);
return (float)((ntabs + 1) * tabWidth);
}
}
}