/* * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ 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. *

* It includes *

*

* Here ia a very simple example, which prints a single node. *

 * Node node = new Circle(100, 200, 200);
 * PrinterJob job = PrinterJob.createPrinterJob();
 * if (job != null) {
 *    boolean success = job.printPage(node);
 *    if (success) {
 *        job.endJob();
 *    }
 * }
 * 
* Points to note *

* 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. *

* 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. *

* 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. *

* 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. *

* @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; 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. *

* The printer 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 createPrinterProperty(Printer printer) { return new SimpleObjectProperty(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 rawObservable) { throw new RuntimeException("Printer property cannot be bound"); } @Override public void bindBidirectional(Property 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 * Printer for this job. */ public final ObjectProperty 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. *

* 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. *

* 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 JobSettings 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. *

* 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. *

* 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. *

* The window owner may be null, but * if it is a visible Window, it will be used as the parent. * @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 is called on the FX application * thread, outside of an event handler. This includes animation callbacks, * properties handlers and 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. *

* 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. *

* 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. *

* The window owner may be null, but * if it is a visible Window, it will be used as the parent. * @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 is called on the FX application * thread, outside of an event handler. This includes animation callbacks, * properties handlers and 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. *

* This method does not update the job configuration. * @param pageLayout to be validated * @return a PageLayout 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. * @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 is called on the FX application * thread, outside of an event handler. This includes animation callbacks, * properties handlers and 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()}. *

* The typical life cycle of a job is as follows : *

*

* 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 = new ReadOnlyObjectWrapper(JobStatus.NOT_STARTED); /** * A read only object property representing the current * JobStatus */ public ReadOnlyObjectProperty jobStatusProperty() { return jobStatus.getReadOnlyProperty(); } /** * Obtain the current status of the job. * @return the current JobStatus */ 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. *

* 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. *

* A return value of false means the job could not be spooled, * or was already completed. *

* Successful completion will also update job status to DONE, * at which point the job can no longer be used. *

* 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(); } }