/*
* Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javafx.print;
import javafx.beans.property.
ObjectProperty;
import javafx.beans.property.
Property;
import javafx.beans.property.
ReadOnlyObjectProperty;
import javafx.beans.property.
ReadOnlyObjectWrapper;
import javafx.beans.property.
SimpleObjectProperty;
import javafx.beans.value.
ObservableValue;
import javafx.scene.
Node;
import javafx.stage.
Window;
import com.sun.javafx.print.
PrinterJobImpl;
import com.sun.javafx.tk.
PrintPipeline;
/**
* PrinterJob is the starting place for JavaFX scenegraph printing.
* <p>
* It includes
* <ul>
* <li>Printer discovery
* <li>Job creation
* <li>Job configuration based on supported printer capabilities
* <li>Page setup
* <li>Rendering of a node hierachy to a page.
* </ul>
* <p>
* Here ia a very simple example, which prints a single node.
* <pre>
* Node node = new Circle(100, 200, 200);
* PrinterJob job = PrinterJob.createPrinterJob();
* if (job != null) {
* boolean success = job.printPage(node);
* if (success) {
* job.endJob();
* }
* }
* </pre>
* <b>Points to note</b>
* <p>
* In the example above the node was not added to a scene.
* Since most printing scenarios are printing content that's either
* not displayed at all, or must be prepared and formatted differently,
* this is perfectly acceptable.
* <p>
* If content that is currently part of a Scene and is being displayed,
* is printed, then because printing a job or even a single page
* of the job may span over multiple screen "pulses" or frames, it is
* important for the application to ensure that the node being printed
* is not updated during the printing process, else partial or smeared
* rendering is probable.
* <p>
* It should be apparent that the same applies even to nodes that are
* not displayed - updating them concurrent with printing them is not
* a good idea.
* <p>
* There is no requirement to do printing on the FX application thread.
* A node may be prepared for printing on any thread, the job may
* be invoked on any thread. However, minimising the amount of work
* done on the FX application thread is generally desirable,
* so as not to affect the responsiveness of the application UI.
* So the recommendation is to perform printing on a new thread
* and let the implementation internally schedule any tasks that
* need to be performed on the FX thread to be run on that thread.
* <p>
* @since JavaFX 8.0
*/
public final class
PrinterJob {
// Delegating all the work keeps whatever classes
// are being used out of the API packages.
private
PrinterJobImpl jobImpl;
private
ObjectProperty<
Printer>
printer;
private
JobSettings settings;
/**
* Factory method to create a job.
* If there are no printers available, this will return null.
* Some platforms may provide a pseudo printer, which creates
* a document. These will be enumerated here so long as the
* platform also enumerates them as if they are printers.
* @return a new PrinterJob instance, or null.
* @throws SecurityException if a job does not have permission
* to initiate a printer job.
*/
public static final
PrinterJob createPrinterJob() {
SecurityManager security =
System.
getSecurityManager();
if (
security != null) {
security.
checkPrintJobAccess();
}
Printer printer =
Printer.
getDefaultPrinter();
if (
printer == null) {
return null;
} else {
return new
PrinterJob(
printer);
}
}
/**
* Factory method to create a job for a specified printer.
* <p>
* The <code>printer</code> argument determines the initial printer
* @param printer to use for the job. If the printer is currently
* unavailable (eg offline) then this may return null.
* @return a new PrinterJob, or null.
* @throws SecurityException if a job does not have permission
* to initiate a printer job.
*/
public static final
PrinterJob createPrinterJob(
Printer printer) {
SecurityManager security =
System.
getSecurityManager();
if (
security != null) {
security.
checkPrintJobAccess();
}
return new
PrinterJob(
printer);
}
private
PrinterJob(
Printer printer) {
this.
printer =
createPrinterProperty(
printer);
settings =
printer.
getDefaultJobSettings();
settings.
setPrinterJob(this);
createImplJob(
printer,
settings);
}
synchronized private
PrinterJobImpl createImplJob(
Printer printer,
JobSettings settings) {
if (
jobImpl == null) {
jobImpl =
PrintPipeline.
getPrintPipeline().
createPrinterJob(this);
}
return
jobImpl;
}
/**
* Updating settings or printer is only allowed on a new job,
* meaning before you start printing or cancel etc.
* The implementation needs to check this wherever job state
* updates are received.
*/
boolean
isJobNew() {
return
getJobStatus() ==
JobStatus.
NOT_STARTED;
}
private
ObjectProperty<
Printer>
createPrinterProperty(
Printer printer) {
return new
SimpleObjectProperty<
Printer>(
printer) {
@
Override
public void
set(
Printer value) {
if (
value ==
get() || !
isJobNew()) {
return;
}
if (
value == null) {
value =
Printer.
getDefaultPrinter();
}
super.set(
value);
jobImpl.
setPrinterImpl(
value.
getPrinterImpl());
settings.
updateForPrinter(
value);
}
@
Override
public void
bind(
ObservableValue<? extends
Printer>
rawObservable) {
throw new
RuntimeException("Printer property cannot be bound");
}
@
Override
public void
bindBidirectional(
Property<
Printer>
other) {
throw new
RuntimeException("Printer property cannot be bound");
}
@
Override
public
Object getBean() {
return
PrinterJob.this;
}
@
Override
public
String getName() {
return "printer";
}
};
}
/**
* Property representing the
* <code>Printer</code> for this job.
*/
public final
ObjectProperty<
Printer>
printerProperty() {
/* The PrinterJob constructor always creates this property,
* so it can be returned directly.
*/
return
printer;
}
/**
* Gets the printer currently associated with this job.
* @return printer for the job.
*/
public synchronized
Printer getPrinter() {
return
printerProperty().
get();
}
/**
* Change the printer for this job.
* If the new printer does not support the current job settings,
* (for example if DUPLEX printing is requested but the new printer
* does not support this), then the values are reset to the default
* for the new printer, or in some cases a similar value. For example
* this might mean REVERSE_LANDSCAPE is updated to LANDSCAPE, however
* this implementation optimisation is allowed, but not required.
* <p>
* The above applies whether the printer is changed by directly calling
* this method, or as a side-effect of user interaction with a print
* dialog.
* <p>
* Setting a null value for printer will install the default printer.
* Setting the current printer has no effect.
* @param printer to be used for this print job.
*/
public synchronized void
setPrinter(
Printer printer) {
printerProperty().
set(
printer);
}
/**
* The <code>JobSettings</code> encapsulates all the API supported job
* configuration options such as number of copies,
* collation option, duplex option, etc.
* The initial values are based on the current settings for
* the initial printer.
* @return current job settings.
*/
public synchronized
JobSettings getJobSettings() {
return
settings;
}
/**
* Displays a Print Dialog.
* Allow the user to update job state such as printer and settings.
* These changes will be available in the appropriate properties
* after the print dialog has returned.
* The print dialog is also typically used to confirm the user
* wants to proceed with printing. This is not binding on the
* application but generally should be obeyed.
* <p>
* In the case that there is no UI available then this method
* returns true, with no options changed, as if the user had
* confirmed to proceed with printing.
* <p>
* If the job is not in a state to display the dialog, such
* as already printing, cancelled or done, then the dialog will
* not be displayed and the method will return false.
* <p>
* The window <code>owner</code> may be null, but
* if it is a visible Window, it will be used as the parent.
* <p>
* This method may be called from any thread. If it is called from the
* JavaFX application thread, then it must either be called from an input
* event handler or from the run method of a Runnable passed to
* {@link javafx.application.Platform#runLater Platform.runLater}.
* It must not be called during animation or layout processing.
*
* @param owner to which to block input, or null.
* @return false if the user opts to cancel printing, or the job
* is not in the new state. That is if it has already started,
* has failed, or has been cancelled, or ended.
* @throws IllegalStateException if this method is called during
* animation or layout processing.
*/
public synchronized boolean
showPrintDialog(
Window owner) {
// TBD handle owner
if (!
isJobNew()) {
return false;
} else {
return
jobImpl.
showPrintDialog(
owner);
}
}
/**
* Displays a Page Setup dialog.
* A page set up dialog is primarily to allow an end user
* to configure the layout of a page. Paper size and orientation
* are the most common and most important components of this.
* <p>
* This will display the most appropriate available dialog for
* this purpose.
* However there may be still be access to other settings,
* including changing the current printer.
* Therefore a side effect of this dialog display method may be to
* update that and any other current job settings.
* The method returns true if the user confirmed the dialog whether or
* not any changes are made.
* <p>
* If the job is not in a state to display the dialog, such
* as already printing, cancelled or done, then the dialog will
* not be displayed and the method will return false.
* <p>
* The window <code>owner</code> may be null, but
* if it is a visible Window, it will be used as the parent.
* <p>
* This method may be called from any thread. If it is called from the FX
* application thread, then it must either be called from an input event
* handler or from the run method of a Runnable passed to
* {@link javafx.application.Platform#runLater Platform.runLater}.
* It must not be called during animation or layout processing.
*
* @param owner to block input, or null.
* @return false if the user opts to cancel the dialog, or the job
* is not in the new state. That is if it has already started,
* has failed, or has been cancelled, or ended.
* @throws IllegalStateException if this method is called during
* animation or layout processing.
*/
public synchronized boolean
showPageSetupDialog(
Window owner) {
// TBD handle owner
if (!
isJobNew()) {
return false;
} else {
return
jobImpl.
showPageDialog(
owner);
}
}
/**
* This method can be used to check if a page configuration
* is possible in the current job configuration. For example
* if the specified paper size is supported. If the original
* PageLayout is supported it will be returned. If not, a new PageLayout
* will be returned that attempts to honour the supplied
* PageLayout, but adjusted to match the current job configuration.
* <p>
* This method does not update the job configuration.
* @param pageLayout to be validated
* @return a <code>PageLayout</code> that is supported in the
* current job configuration.
* @throws NullPointerException if the pageLayout parameter is null.
*/
synchronized
PageLayout validatePageLayout(
PageLayout pageLayout) {
if (
pageLayout == null) {
throw new
NullPointerException("pageLayout cannot be null");
}
return
jobImpl.
validatePageLayout(
pageLayout);
}
/**
* Print the specified node using the specified page layout.
* The page layout will override the job default for this page only.
* If the job state is CANCELED, ERROR or DONE, this method will
* return false.
* <p>
* This method may be called from any thread. If it is called from the FX
* application thread, then it must either be called from an input event
* handler or from the run method of a Runnable passed to
* {@link javafx.application.Platform#runLater Platform.runLater}.
* It must not be called during animation or layout processing.
*
* @param pageLayout Layout for this page.
* @param node The node to print.
* @return whether rendering was successful.
* @throws NullPointerException if either parameter is null.
* @throws IllegalStateException if this method is called during
* animation or layout processing.
*/
public synchronized boolean
printPage(
PageLayout pageLayout,
Node node) {
if (
jobStatus.
get().
ordinal() >
JobStatus.
PRINTING.
ordinal()) {
return false;
}
if (
jobStatus.
get() ==
JobStatus.
NOT_STARTED) {
jobStatus.
set(
JobStatus.
PRINTING);
}
if (
pageLayout == null ||
node == null) {
jobStatus.
set(
JobStatus.
ERROR);
throw new
NullPointerException("Parameters cannot be null");
}
boolean
rv =
jobImpl.
print(
pageLayout,
node);
if (!
rv) {
jobStatus.
set(
JobStatus.
ERROR);
}
return
rv;
}
/**
* Print the specified node. The page layout is the job default.
* If the job state is CANCELED, ERROR or DONE, this method will
* return false.
* @param node The node to print.
* @return whether rendering was successful.
* @throws NullPointerException if the node parameter is null.
*/
public synchronized boolean
printPage(
Node node) {
return
printPage(
settings.
getPageLayout(),
node);
}
/**
* An enum class used in reporting status of a print job.
* Applications can listen to the job status via the
* {@link #jobStatus jobStatus} property, or may query it directly
* using {@link javafx.print.PrinterJob#getJobStatus getJobStatus()}.
* <p>
* The typical life cycle of a job is as follows :
* <ul>
* <li>job will be created with status <code>NOT_STARTED</code> and
* will stay there during configuration via dialogs etc.
* <li>job will enter state <code>PRINTING</code> when the first page
* is printed.
* <li>job will enter state <code>DONE</code> once the job is
* successfully completed without being cancelled or encountering
* an error. The job is now completed.
* <li>A job that encounters an <code>ERROR</code> or is
* <code>CANCELED</code> is also considered completed.
* </ul>
* <p>
* A job may not revert to an earlier status in its life cycle and
* the current job state affects operations that may be performed.
* For example a job may not begin printing again if it has previously
* passed that state and entered any of the termination states.
*
* @since JavaFX 8.0
*/
public static enum
JobStatus {
/**
* The new job status. May display print dialogs and
* configure the job and initiate printing.
*/
NOT_STARTED,
/**
* The job has requested to print at least one page,
* and has not terminated printing. May no longer
* display print dialogs.
*/
PRINTING,
/**
* The job has been cancelled by the application.
* May not display dialogs or initiate printing.
* Job should be discarded. There is no need to
* call endJob().
*/
CANCELED,
/**
* The job encountered an error.
* Job should be discarded. There is no need to
* call endJob().
*/
ERROR,
/**
* The job initiated printing and later called endJob()
* which reported success. The job can be discarded
* as it cannot be re-used.
*/
DONE
};
private
ReadOnlyObjectWrapper<
JobStatus>
jobStatus =
new
ReadOnlyObjectWrapper(
JobStatus.
NOT_STARTED);
/**
* A read only object property representing the current
* <code>JobStatus</code>
*/
public
ReadOnlyObjectProperty<
JobStatus>
jobStatusProperty() {
return
jobStatus.
getReadOnlyProperty();
}
/**
* Obtain the current status of the job.
* @return the current <code>JobStatus</code>
*/
public
JobStatus getJobStatus() {
return
jobStatus.
get();
}
/**
* Cancel the underlying print job at the earliest opportunity.
* It may return immediately without waiting for the job cancellation
* to be complete in case this would block the FX user thread
* for any period of time.
* If printing is in process at that time, then typically
* this means cancellation is after the current page is rendered.
* The job status is updated to CANCELED only once that has happened.
* Thus determining that the job is CANCELED requires monitoring
* the job status.
* <p>
* The call has no effect if the job has already been requested
* to be CANCELED, or is in the state ERROR or DONE.
* For example it will not de-queue from the printer a job that
* has already been spooled for printing.
* Once a job is cancelled, it is not valid to call any methods
* which render new content or change job state.
*/
public void
cancelJob() {
if (
jobStatus.
get().
ordinal() <=
JobStatus.
PRINTING.
ordinal()) {
jobStatus.
set(
JobStatus.
CANCELED);
jobImpl.
cancelJob();
}
}
/**
* If the job can be successfully spooled to the printer queue
* this will return true. Note : this does not mean the job already
* printed as that could entail waiting for minutes or longer,
* even where implementable.
* <p>
* A return value of false means the job could not be spooled,
* or was already completed.
* <p>
* Successful completion will also update job status to <code>DONE</code>,
* at which point the job can no longer be used.
* <p>
* Calling endJob() on a job for which no pages have been printed
* is equivalent to calling {code cancelJob()}.
* @return true if job is spooled, false if its not, or the job
* was already in a completed state.
*/
public synchronized boolean
endJob() {
if (
jobStatus.
get() ==
JobStatus.
NOT_STARTED) {
cancelJob();
return false;
} else if (
jobStatus.
get() ==
JobStatus.
PRINTING) {
boolean
rv =
jobImpl.
endJob();
jobStatus.
set(
rv ?
JobStatus.
DONE :
JobStatus.
ERROR);
return
rv;
} else {
return false;
}
}
@
Override
public
String toString() {
return "JavaFX PrinterJob " +
getPrinter() + "\n" +
getJobSettings() + "\n" +
"Job Status = " +
getJobStatus();
}
}