/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.openide.text;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Font;
import java.awt.Dialog;
import java.awt.print.Printable;
import java.awt.print.PageFormat;
import java.awt.print.PrinterGraphics;
import java.awt.print.PrinterException;
import java.awt.print.PrinterAbortException;
import java.awt.print.PrinterJob;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextLayout;
import java.awt.font.FontRenderContext;
import java.text.AttributedCharacterIterator;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.swing.text.Document;
import javax.swing.text.BadLocationException;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
/** The class creates from an instance of AttributedCharacterIterator
* a java.awt.print.Pageable object.
*
* @author Ales Novak
*/
final class DefaultPrintable extends Object implements Printable {
/**
* for each item is created new LineBreakMeasurer
*/
private AttributedCharacterIterator[] styledTexts;
/** expected page */
// private int pageno;
/** Start page */
private int startPage = -1;
/** created text layouts */
private ArrayList textLayouts;
/** page indices to textLayouts list */
private int[] pageIndices;
/** pageIndices size */
private int pageIndicesSize;
/** iterator over textLayouts */
private int currentLayout;
/** curent styledText entry */
private int currentStyledText;
/** current LineBreakMeasurer */
private LineBreakMeasurer lineBreakMeasurer;
/** maximal page */
private int maxPage;
// not used now
/** current line */
private int currentLine;
/** text layouts that starts new line (those that were not created thanks to wrapping) */
private ArrayList startLayouts;
/** page to line indexes (page 5 starts with line 112) */
private int[] lineIndices; // pageIndicesSize
/** Number of args. */
private static final int ARG_SIZE = 3;
/** Arguments for page. */
private Object[] pageArgs;
/** Header of each page. */
private MessageFormat header;
/** Should be header printed? */
private boolean printHeader;
/** Bottom of each page. */
private MessageFormat footer;
/** Should be footer printed? */
private boolean printFooter;
/** Options */
private static PrintSettings printSettings;
/** CancellationPanel */
private CancellationPanel cancellationPanel;
/** Dialog */
private Dialog cancellationDialog;
/**
* @param attrs an AttributedCharacterIterator
* @param filename
*/
private DefaultPrintable(AttributedCharacterIterator[] iter, String filename) {
if ((iter == null) ||
(iter.length == 0)) {
throw new IllegalArgumentException();
}
if (printSettings == null) {
printSettings = (PrintSettings) PrintSettings.findObject(PrintSettings.class, true);
}
// bugfix for sun.awt.Bidi line 250
replaceEmptyIterators(iter);
styledTexts = iter;
// pageno = 0;
textLayouts = new ArrayList(100); // 100 lines
pageIndices = new int[50];
pageIndicesSize = 0;
currentLayout = 0;
currentStyledText = 0;
lineBreakMeasurer = null;
maxPage = Integer.MAX_VALUE;
// not used
currentLine = 0;
startLayouts = new ArrayList(10); // 10 lines
lineIndices = new int[pageIndices.length];
pageArgs = new Object[ARG_SIZE];
pageArgs[2] = filename;
pageArgs[1] = new Date(System.currentTimeMillis());
header = new MessageFormat(getHeaderFormat());
printHeader = !getHeaderFormat().equals(""); // NOI18N
footer = new MessageFormat(getFooterFormat());
printFooter = !getFooterFormat().equals(""); // NOI18N
}
/**
* @param doc printed document
*/
public DefaultPrintable(Document doc) {
this(getIterators(doc), getFilename(doc));
}
/**
* Prints a page.
*
* @param g a Graphics
* @param pf a PageFormat
* @param pageNo Which page?
*/
public int print(Graphics g, PageFormat pf, int pageNo) throws PrinterException {
boolean processDummy = false;
if(startPage == -1) {
processDummy = true;
startPage = pageNo;
}
if(processDummy) {
for(int i = 0; i < startPage; i++) {
// XXX #21245 Processes dummy pages (first pages to not print).
// PENDING shuold be made better+faster way to skip
// processing of such pages (then this - hot fix).
printImpl(g, pf, i, false);
}
}
return printImpl(g, pf, pageNo, true);
}
private int printImpl(
Graphics g,
PageFormat pf,
int pageNo,
boolean print
) throws PrinterException {
if (pageNo > maxPage) {
closeDialog();
return Printable.NO_SUCH_PAGE;
} else if(pageNo < 0){
closeDialog();
throw new IllegalArgumentException("Illegal page number="+pageNo); // NOI18N
}
// stop if cancelled
if ((g instanceof PrinterGraphics) && isCancelled(((PrinterGraphics) g).getPrinterJob())) {
closeDialog();
throw new PrinterAbortException();
}
if ((cancellationPanel == null) && (g instanceof PrinterGraphics)) {
// [TODO] - commented out since the awt API does not allow proper handling
// of the dialog - e.g when am I to close it?
PrinterJob pJob = ((PrinterGraphics) g).getPrinterJob();
createCancellationPanel(pJob);
}
if(cancellationPanel != null) {
int pageNumber = (print ? pageNo : startPage);
cancellationPanel.setPageno(pageNumber);
packDialog();
}
// line numbers init
int startLine = 0;
int correction = 3; // magic - take pencil, paper, and start measuring.
// if (lineNumbers()) { // not used
/*
lineNo = Integer.toString(startLine = page2Line(pageNo));
// correction may not be ok
if (startLine < 100) {
lineNo = lineNo + " ";
} else {
lineNo = lineNo + " ";
}
correction = g.getFontMetrics().stringWidth(lineNo);
*/
// }
g.setColor(Color.black);
final Graphics2D graphics = (Graphics2D) g;
final Point2D.Float pen = new Point2D.Float(getImageableXPatch(pf),
getImageableYPatch(pf)
);
/* System.out.println("PEN IS: " + pen);
pen = new Point2D.Float((float) pf.getImageableWidth(),
(float) pf.getImageableHeight()
);
System.out.println("END IS: " + pen);
Paper paper = pf.getPaper();
System.out.println("DEF IS: " + paper.getHeight() + ", " + paper.getWidth());
*/
// header & footer init
pageArgs[0] = new Integer(pageNo + 1); // pages numbered from 1
float pageBreakCorrection = 0.0F;
TextLayout headerString = null;
TextLayout footerString = null;
if(printHeader) {
headerString = new TextLayout(header.format(pageArgs),
getHeaderFont(),
graphics.getFontRenderContext());
pageBreakCorrection += headerString.getAscent()
+ (headerString.getDescent()
+ headerString.getLeading()) * 2;
}
if(printFooter) {
footerString = new TextLayout(footer.format(pageArgs),
getFooterFont(),
graphics.getFontRenderContext());
pageBreakCorrection += footerString.getAscent() * 2
+ footerString.getDescent()
+ footerString.getLeading();
}
// for now suppose that getImageableWidthPatch(pf) is always
// the same during the same print job
final float wrappingWidth = (wrap() ? (float) pf.getImageableWidth() - correction :
Float.MAX_VALUE);
final float pageBreak = ((float) pf.getImageableHeight()) + ((float)pf.getImageableY()) - pageBreakCorrection;
final FontRenderContext frCtx = graphics.getFontRenderContext();
boolean pageExists = false;
// page rendering
for (TextLayout layout = layoutForPage(pageNo, wrappingWidth, frCtx);
(pen.y < pageBreak);
layout = nextLayout(wrappingWidth, frCtx)
) {
if (layout == null) {
maxPage = pageNo;
break;
}
if (!pageExists) {
// draw header
if (printHeader && headerString != null) {
pen.y += headerString.getAscent();
float center = computeStart(headerString.getBounds(), (float) pf.getImageableWidth(), getHeaderAlignment());
float dx = (headerString.isLeftToRight() ? center: wrappingWidth - headerString.getAdvance() - center);
if(print) {
headerString.draw(graphics, pen.x + dx, pen.y);
}
pen.y += (headerString.getDescent() + headerString.getLeading()) * 2;
}
pageExists = true;
}
pen.y += layout.getAscent() * getLineAscentCorrection();
// line number handling
// if (lineNumbers() && isNewline(layout, startLine)) {
/*
lineNo = Integer.toString(++startLine); // + 1 -> lines starts from 1
if (startLine < 100) {
lineNo = lineNo + " ";
} else {
lineNo = lineNo + " ";
}
// graphics.drawString(lineNo, (int) pen.x, (int) pen.y);
TextLayout tl = new TextLayout(lineNo, lineNumbersFont(), graphics.getFontRenderContext());
tl.draw(graphics, pen.x, pen.y);
*/
// }
float dx = (layout.isLeftToRight() ? 0 : wrappingWidth - layout.getAdvance());
if(print) {
layout.draw(graphics, correction + pen.x + dx, pen.y);
}
pen.y += (layout.getDescent() + layout.getLeading()) * getLineAscentCorrection();
}
// draw footer
if (printFooter && pageExists && footerString != null) {
pen.y = pageBreak;
pen.y += footerString.getAscent() * 2;
float center = computeStart(footerString.getBounds(), (float) pf.getImageableWidth(), getFooterAlignment());
float dx = (footerString.isLeftToRight() ? 0 + center : wrappingWidth - footerString.getAdvance() - center);
if(print) {
footerString.draw(graphics, pen.x + dx, pen.y);
}
}
// stop if cancelled
if ((g instanceof PrinterGraphics) && isCancelled(((PrinterGraphics) g).getPrinterJob())) {
closeDialog();
throw new PrinterAbortException();
}
// at least one layout draw?
if (!pageExists) {
closeDialog();
return Printable.NO_SUCH_PAGE;
} else {
return Printable.PAGE_EXISTS;
}
}
/* for following two methods:
* windows page setup dialog behaves incorrectly for LANDSCAPE format
* the x coordinate is influenced by RIGHT margin instead of LEFT margin
* the y coordinate ... BOTTOM instead of TOP
* #3732
*/
/** Patch for a bug in the PageFormat class
* @param pf PageFormat
* @return imageable x coordination for this page format
*/
private float getImageableXPatch(PageFormat pf) {
if (pf.getOrientation() == PageFormat.LANDSCAPE) {
double ret = pf.getPaper().getHeight() - (pf.getImageableX() + pf.getImageableWidth());
return (float) Math.round(ret);
} else {
return (float) pf.getImageableX();
}
}
/** Patch for a bug in the PageFormat class
* @param pf PageFormat
* @return imageable y coordination for this page format
*/
private float getImageableYPatch(PageFormat pf) {
if (pf.getOrientation() == PageFormat.LANDSCAPE) {
double ret = pf.getPaper().getWidth() - (pf.getImageableY() + pf.getImageableHeight());
return (float) Math.round(ret);
} else {
return (float) pf.getImageableY();
}
}
/** Translates given page number to line number.
* @param pageNo
* @return number of first line on the page
* /
private int page2Line(int pageNo) {
if (pageNo == 0) {
return 0;
} else {
return (pageNo == pageIndicesSize ? Math.max(startLayouts.size() - 1, 0) : lineIndices[pageNo]);
}
}
*/
/**
* @param tl a TextLayout
* @param currentLine
* @return <tt>true</tt> iff <tt>tl</tt> is a TextLayout that does not represent wrapped line
*/
private boolean isNewline(TextLayout tl, int currentLine) {
if (currentLine >= startLayouts.size()) {
return false; // wrapping appeared
} else {
return startLayouts.get(currentLine) == tl;
}
}
/** Computes alignment for a TextLayout with given bounds on the page with given width
* and for given alignment policy.
*
* @param rect Bounds of a TextLayout
* @param width page width
* @param alignment one of @see PageSettings#LEFT @see PageSettings#CENTER @see PageSettings#RIGHT
*/
private static float computeStart(Rectangle2D rect, float width, int alignment) {
float x;
if (rect instanceof Rectangle2D.Float) {
x = ((Rectangle2D.Float) rect).width;
} else {
x = (float) ((Rectangle2D.Double) rect).width;
}
if (x >= width) {
return 0;
}
switch (alignment) {
case PrintSettings.LEFT : return 0;
case PrintSettings.RIGHT : return (width - x);
default: return (width - x) / 2;
}
}
/**
* @param wrappingWidth width of the layout
* @param frCtx for possible new instance of LineBreakMeasurer
* @return next TextLayout that is to be rendered
*/
private TextLayout nextLayout(float wrappingWidth, FontRenderContext frc) {
TextLayout l;
if (currentLayout == textLayouts.size()) {
LineBreakMeasurer old = lineBreakMeasurer;
LineBreakMeasurer measurer = getMeasurer(frc);
if (measurer == null) {
return null;
}
l = measurer.nextLayout(wrappingWidth);
textLayouts.add(l);
if (old != measurer) { // new line
startLayouts.add(l);
}
} else {
l = (TextLayout) textLayouts.get(currentLayout);
}
currentLayout++; // advance to next
return l;
}
/** Sets @see #currentLayout variable then calls nextLayout.
* @param pageNo searched page
* @param wrappingWidth width of the layout
* @param frCtx for possible new instance of LineBreakMeasurer
* @return next TextLayout that is to be rendered
*/
private TextLayout layoutForPage(int pageNo, float wrappingWidth, FontRenderContext frc) {
if (pageNo > pageIndicesSize + 1) {
throw new IllegalArgumentException("Page number " + pageNo // NOI18N
+" is bigger than array size " + (pageIndicesSize + 1)); // NOI18N
}
// first request for a page // pageNo==3 -> fourth page to print
if (pageNo == pageIndicesSize) {
// small array?
if (pageIndicesSize >= pageIndices.length) {
pageIndices = increaseArray(pageIndices);
lineIndices = increaseArray(lineIndices);
}
// layouts for given page starts at:
pageIndices[pageIndicesSize] = Math.max(textLayouts.size() - 1, 0);
// remember - in the for loop above last layout is not printed
// if page breaks
lineIndices[pageIndicesSize++] = Math.max(startLayouts.size() - 1, 0);
}
currentLayout = pageIndices[pageNo]; // set the iterator
return nextLayout(wrappingWidth, frc); // iterate
}
/** Called only if new TextLayouts are in need.
* @param frc is used for possible new LineBreakMeasurer instance
* @return current LineBreakMeasurer or <tt>null</tt> if no is available.
*/
private LineBreakMeasurer getMeasurer(FontRenderContext frc) {
if (lineBreakMeasurer == null) { // first page to print
lineBreakMeasurer = new LineBreakMeasurer(styledTexts[currentStyledText],
frc);
// no layouts available in this measurer?
} else if (lineBreakMeasurer.getPosition() >= styledTexts[currentStyledText].getEndIndex()) {
// next measurer is not available?
if (currentStyledText == styledTexts.length - 1) {
return null; // everything is printed
} else { // use next styledTexts entry
lineBreakMeasurer = new LineBreakMeasurer(styledTexts[++currentStyledText],
frc);
}
}
return lineBreakMeasurer;
}
// ------------------ options -----------------
/** @return true iff wrapping is on*/
private static boolean wrap() {
return printSettings.getWrap();
}
/** @return String describing header */
private static String getHeaderFormat() {
return printSettings.getHeaderFormat();
}
/** @return String describing footer */
private static String getFooterFormat() {
return printSettings.getFooterFormat();
}
/** @return font for header */
private static Font getHeaderFont() {
return printSettings.getHeaderFont();
}
/** @return font for footer */
private static Font getFooterFont() {
return printSettings.getFooterFont();
}
/** @return an alignment constant for footer */
private static int getFooterAlignment() {
return printSettings.getFooterAlignment();
}
/** @return an alignment constant for header */
private static int getHeaderAlignment() {
return printSettings.getHeaderAlignment();
}
/** @return a line ascent correction */
private static float getLineAscentCorrection() {
return printSettings.getLineAscentCorrection();
}
/** @return false */
private static boolean lineNumbers() {
return false;
}
// not used
private static Font lineNumbersFont() {
return new Font("Courier", java.awt.Font.PLAIN, 6); // NOI18N
}
// ----------------- options end --------------
/** Creates new AttributedCharacterIterator for plain text.
*
* @return an AttributedCharacterIterator
*/
private static AttributedCharacterIterator[] getIterators(Document doc) {
if (doc instanceof NbDocument.Printable) {
return ((NbDocument.Printable) doc).createPrintIterators();
}
// load options
java.awt.Font f = new java.awt.Font("Courier", java.awt.Font.PLAIN, 8); // NOI18N
AttributedCharacters achs = null;
char[] chars = null;
ArrayList iterators = new ArrayList(300);
try {
String document = doc.getText(0, doc.getLength());
// now chars are filled from the document
int firstCharInDoc = 0;
for (int i = 0; i < document.length(); i++) { // search for new lines
if (document.charAt(i) == '\n') {
chars = new char[i - firstCharInDoc + 1];
document.getChars(firstCharInDoc, chars.length + firstCharInDoc, chars, 0);
achs = new AttributedCharacters();
achs.append(chars, f, Color.black);
iterators.add(achs.iterator()); // new iterator for new line
firstCharInDoc = i + 1;
}
}
} catch (BadLocationException e){
ErrorManager.getDefault().notify(e);
}
AttributedCharacterIterator[] iters = new AttributedCharacterIterator[iterators.size()];
iterators.toArray(iters);
return iters;
}
/**
* @param doc
* @return filename from which the document is loaded.
*/
private static String getFilename(Document doc) {
String ret = (String) doc.getProperty(javax.swing.text.Document.TitleProperty);
return (ret == null ? "UNKNOWN" : ret); // NOI18N
}
/** Doubles given array. The old one is then copied into the new one.
* @return new int array
*/
private static int[] increaseArray(int[] old) {
int[] ret = new int[2 * old.length];
System.arraycopy(old, 0, ret, 0, old.length);
return ret;
}
// ------------------ cancellation dialog ---------------------------
/** Creates cancellation dialog.
* @param job PrinterJob
*/
private void createCancellationPanel(final PrinterJob job) {
cancellationPanel= new CancellationPanel(job);
DialogDescriptor ddesc = new DialogDescriptor(
cancellationPanel,
NbBundle.getMessage(PrintSettings.class, "CTL_Print_cancellation"),
false,
new Object[] {NbBundle.getMessage(PrintSettings.class, "CTL_Cancel")},
NbBundle.getMessage(PrintSettings.class, "CTL_Cancel"),
DialogDescriptor.BOTTOM_ALIGN,
new HelpCtx (DefaultPrintable.class.getName () + ".cancelDialog"), // NOI18N
new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent ev) {
setCancelled(job);
closeDialog();
}
}
);
setDialog(DialogDisplayer.getDefault().createDialog(ddesc));
}
/** Closes cancellationDialog */
void closeDialog() {
if (cancellationDialog != null) {
cancellationDialog.setVisible(false);
cancellationDialog.dispose();
}
}
/** @param <tt>d</tt> New value of <tt>cancellationDialog</tt>*/
void setDialog(Dialog d) {
d.show();
d.pack();
cancellationDialog = d;
}
/** packs the dialog */
void packDialog() {
if (cancellationDialog != null) {
cancellationDialog.pack();
}
}
/** Marks this job as cancelled.
* @param <tt>job</tt>
*/
void setCancelled(PrinterJob job) {
job.cancel();
}
/** @return <tt>true</tt> iff the job was cancelled */
boolean isCancelled(PrinterJob job) {
return job.isCancelled();
}
/** Replaces an empty iterator by an iterator conatining one space. */
private static void replaceEmptyIterators(AttributedCharacterIterator[] iters) {
for (int i = 0; i < iters.length; i++) {
AttributedCharacterIterator achit = iters[i];
if (achit.getBeginIndex() == achit.getEndIndex()) {
AttributedCharacters at = new AttributedCharacters();
at.append(' ', getFontInstance(), Color.white);
iters[i] = at.iterator();
}
}
}
/** CancellationPanel class allows user to cancel current PrinterJob */
static final class CancellationPanel extends javax.swing.JPanel {
/** Controlled PrinterJob */
private final PrinterJob job;
/** Label indicating progress. */
private final javax.swing.JLabel printProgress;
/** Format of displayed text. */
private final MessageFormat format;
/** Parameters for <tt>format</tt>. */
private final Object[] msgParams;
static final long serialVersionUID =-6419253408585188541L;
/**
* @param <tt>job</tt> PrinterJob
* @exception IllegalArgumentException is thrown if <tt>job</tt> is <tt>null</tt>.
*/
public CancellationPanel(PrinterJob job) {
if (job == null) {
throw new IllegalArgumentException();
}
this.job = job;
format = new MessageFormat(NbBundle.getMessage(PrintSettings.class, "CTL_Print_progress"));
msgParams = new Object[1];
setLayout(new java.awt.BorderLayout());
setBorder(new javax.swing.border.EmptyBorder(12, 12, 0, 12));
printProgress = new javax.swing.JLabel();
printProgress.setHorizontalAlignment(javax.swing.JLabel.CENTER);
add(printProgress);
}
/** Advances progress.
* @param <tt>pageno</tt> Page number that was printed.
*/
public void setPageno(int pageno) {
msgParams[0] = new Integer(pageno + 1);
printProgress.setText(format.format(msgParams));
getAccessibleContext().setAccessibleDescription(printProgress.getText());
}
}
/** Font for CancellationDialog */
private static Font fontInstance;
/** @return cached font instance */
static Font getFontInstance() {
if (fontInstance == null) {
fontInstance = new java.awt.Font("Dialog", java.awt.Font.PLAIN, 14); // NOI18N
}
return fontInstance;
}
}