1 /*
   2  * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.print;
  27 
  28 import javafx.beans.property.ObjectProperty;
  29 import javafx.beans.property.Property;
  30 import javafx.beans.property.ReadOnlyObjectProperty;
  31 import javafx.beans.property.ReadOnlyObjectWrapper;
  32 import javafx.beans.property.SimpleObjectProperty;
  33 import javafx.beans.value.ObservableValue;
  34 import javafx.scene.Node;
  35 import javafx.stage.Window;
  36 
  37 import com.sun.javafx.print.PrinterJobImpl;
  38 import com.sun.javafx.tk.PrintPipeline;
  39 
  40 /**
  41  * PrinterJob is the starting place for JavaFX scenegraph printing.
  42  * <p>
  43  * It includes
  44  * <ul>
  45  * <li>Printer discovery
  46  * <li>Job creation
  47  * <li>Job configuration based on supported printer capabilities
  48  * <li>Page setup
  49  * <li>Rendering of a node hierachy to a page.
  50  * </ul>
  51  * <p>
  52  * Here ia a very simple example, which prints a single node.
  53  * <pre>
  54  * Node node = new Circle(100, 200, 200);
  55  * PrinterJob job = PrinterJob.createPrinterJob();
  56  * if (job != null) {
  57  *    boolean success = job.printPage(node);
  58  *    if (success) {
  59  *        job.endJob();
  60  *    }
  61  * }
  62  * </pre>
  63  * <b>Points to note</b>
  64  * <p>
  65  * In the example above the node was not added to a scene.
  66  * Since most printing scenarios are printing content that's either
  67  * not displayed at all, or must be prepared and formatted differently,
  68  * this is perfectly acceptable.
  69  * <p>
  70  * If content that is currently part of a Scene and is being displayed,
  71  * is printed, then because printing a job or even a single page
  72  * of the job may span over multiple screen "pulses" or frames, it is
  73  * important for the application to ensure that the node being printed
  74  * is not updated during the printing process, else partial or smeared
  75  * rendering is probable.
  76  * <p>
  77  * It should be apparent that the same applies even to nodes that are
  78  * not displayed - updating them concurrent with printing them is not
  79  * a good idea.
  80  * <p>
  81  * There is no requirement to do printing on the FX application thread.
  82  * A node may be prepared for printing on any thread, the job may
  83  * be invoked on any thread. However, minimising the amount of work
  84  * done on the FX application thread is generally desirable,
  85  * so as not to affect the responsiveness of the application UI.
  86  * So the recommendation is to perform printing on a new thread
  87  * and let the implementation internally schedule any tasks that
  88  * need to be performed on the FX thread to be run on that thread.
  89  * <p>
  90  * @since JavaFX 8.0
  91  */
  92 
  93 public final class PrinterJob {
  94 
  95     // Delegating all the work keeps whatever classes
  96     // are being used out of the API packages.
  97     private PrinterJobImpl jobImpl;
  98 
  99     private ObjectProperty<Printer> printer;
 100 
 101     private JobSettings settings;
 102 
 103     /**
 104      * Factory method to create a job.
 105      * If there are no printers available, this will return null.
 106      * Some platforms may provide a pseudo printer, which creates
 107      * a document. These will be enumerated here so long as the
 108      * platform also enumerates them as if they are printers.
 109      * @return a new PrinterJob instance, or null.
 110      * @throws SecurityException if a job does not have permission
 111      * to initiate a printer job.
 112      */
 113     public static final PrinterJob createPrinterJob() {
 114         SecurityManager security = System.getSecurityManager();
 115         if (security != null) {
 116             security.checkPrintJobAccess();
 117         }
 118         Printer printer = Printer.getDefaultPrinter();
 119         if (printer == null) {
 120             return null;
 121         } else {
 122             return new PrinterJob(printer);
 123         }
 124     }
 125 
 126     /**
 127      * Factory method to create a job for a specified printer.
 128      * <p>
 129      * The <code>printer</code> argument determines the initial printer
 130      * @param printer to use for the job. If the printer is currently
 131      * unavailable (eg offline) then this may return null.
 132      * @return a new PrinterJob, or null.
 133      * @throws SecurityException if a job does not have permission
 134      * to initiate a printer job.
 135      */
 136     public static final PrinterJob createPrinterJob(Printer printer) {
 137         SecurityManager security = System.getSecurityManager();
 138         if (security != null) {
 139             security.checkPrintJobAccess();
 140         }
 141         return new PrinterJob(printer);
 142     }
 143 
 144     private PrinterJob(Printer printer) {
 145 
 146         this.printer = createPrinterProperty(printer);
 147         settings = printer.getDefaultJobSettings();
 148         settings.setPrinterJob(this);
 149         createImplJob(printer, settings);
 150     }
 151 
 152     synchronized private PrinterJobImpl createImplJob(Printer printer,
 153                                                       JobSettings settings) {
 154         if (jobImpl == null) {
 155             jobImpl = PrintPipeline.getPrintPipeline().createPrinterJob(this);
 156         }
 157         return jobImpl;
 158     }
 159 
 160     /**
 161      * Updating settings or printer is only allowed on a new job,
 162      * meaning before you start printing or cancel etc.
 163      * The implementation needs to check this wherever job state
 164      * updates are received.
 165      */
 166     boolean isJobNew() {
 167         return getJobStatus() == JobStatus.NOT_STARTED;
 168     }
 169 
 170     private ObjectProperty<Printer> createPrinterProperty(Printer printer) {
 171 
 172         return new SimpleObjectProperty<Printer>(printer) {
 173 
 174             @Override
 175             public void set(Printer value) {
 176                 if (value == get() || !isJobNew()) {
 177                     return;
 178                 }
 179                 if (value == null) {
 180                     value = Printer.getDefaultPrinter();
 181                 }
 182                 super.set(value);
 183                 jobImpl.setPrinterImpl(value.getPrinterImpl());
 184                 settings.updateForPrinter(value);
 185             }
 186 
 187             @Override
 188             public void bind(ObservableValue<? extends Printer> rawObservable) {
 189                 throw new RuntimeException("Printer property cannot be bound");
 190             }
 191 
 192             @Override
 193             public void bindBidirectional(Property<Printer> other) {
 194                 throw new RuntimeException("Printer property cannot be bound");
 195             }
 196 
 197             @Override
 198             public Object getBean() {
 199                 return PrinterJob.this;
 200             }
 201 
 202             @Override
 203             public String getName() {
 204                 return "printer";
 205             }
 206         };
 207     }
 208 
 209     /**
 210      * Property representing the
 211      * <code>Printer</code> for this job.
 212      * @return the <code>Printer</code> for this job
 213      */
 214     public final ObjectProperty<Printer> printerProperty() {
 215         /* The PrinterJob constructor always creates this property,
 216          * so it can be returned directly.
 217          */
 218         return printer;
 219     }
 220 
 221     /**
 222      * Gets the printer currently associated with this job.
 223      * @return printer for the job.
 224      */
 225     public synchronized Printer getPrinter() {
 226         return printerProperty().get();
 227     }
 228 
 229     /**
 230      * Change the printer for this job.
 231      * If the new printer does not support the current job settings,
 232      * (for example if DUPLEX printing is requested but the new printer
 233      * does not support this), then the values are reset to the default
 234      * for the new printer, or in some cases a similar value. For example
 235      * this might mean REVERSE_LANDSCAPE is updated to LANDSCAPE, however
 236      * this implementation optimisation is allowed, but not required.
 237      * <p>
 238      * The above applies whether the printer is changed by directly calling
 239      * this method, or as a side-effect of user interaction with a print
 240      * dialog.
 241      * <p>
 242      * Setting a null value for printer will install the default printer.
 243      * Setting the current printer has no effect.
 244      * @param printer to be used for this print job.
 245      */
 246     public synchronized void setPrinter(Printer printer) {
 247          printerProperty().set(printer);
 248     }
 249 
 250     /**
 251      * The <code>JobSettings</code> encapsulates all the API supported job
 252      * configuration options such as number of copies,
 253      * collation option, duplex option, etc.
 254      * The initial values are based on the current settings for
 255      * the initial printer.
 256      * @return current job settings.
 257      */
 258     public synchronized JobSettings getJobSettings() {
 259         return settings;
 260     }
 261 
 262     /**
 263      * Displays a Print Dialog.
 264      * Allow the user to update job state such as printer and settings.
 265      * These changes will be available in the appropriate properties
 266      * after the print dialog has returned.
 267      * The print dialog is also typically used to confirm the user
 268      * wants to proceed with printing. This is not binding on the
 269      * application but generally should be obeyed.
 270      * <p>
 271      * In the case that there is no UI available then this method
 272      * returns true, with no options changed, as if the user had
 273      * confirmed to proceed with printing.
 274      * <p>
 275      * If the job is not in a state to display the dialog, such
 276      * as already printing, cancelled or done, then the dialog will
 277      * not be displayed and the method will return false.
 278      * <p>
 279      * The window <code>owner</code> may be null, but
 280      * if it is a visible Window, it will be used as the parent.
 281      * <p>
 282      * This method may be called from any thread. If it is called from the
 283      * JavaFX application thread, then it must either be called from an input
 284      * event handler or from the run method of a Runnable passed to
 285      * {@link javafx.application.Platform#runLater Platform.runLater}.
 286      * It must not be called during animation or layout processing.
 287      *
 288      * @param owner to which to block input, or null.
 289      * @return false if the user opts to cancel printing, or the job
 290      * is not in the new state. That is if it has already started,
 291      * has failed, or has been cancelled, or ended.
 292      * @throws IllegalStateException if this method is called during
 293      * animation or layout processing.
 294      */
 295     public synchronized boolean showPrintDialog(Window owner) {
 296         // TBD handle owner
 297         if (!isJobNew()) {
 298             return false;
 299         } else {
 300             return jobImpl.showPrintDialog(owner);
 301         }
 302     }
 303 
 304     /**
 305      * Displays a Page Setup dialog.
 306      * A page set up dialog is primarily to allow an end user
 307      * to configure the layout of a page. Paper size and orientation
 308      * are the most common and most important components of this.
 309      * <p>
 310      * This will display the most appropriate available dialog for
 311      * this purpose.
 312      * However there may be still be access to other settings,
 313      * including changing the current printer.
 314      * Therefore a side effect of this dialog display method may be to
 315      * update that and any other current job settings.
 316      * The method returns true if the user confirmed the dialog whether or
 317      * not any changes are made.
 318      * <p>
 319      * If the job is not in a state to display the dialog, such
 320      * as already printing, cancelled or done, then the dialog will
 321      * not be displayed and the method will return false.
 322      * <p>
 323      * The window <code>owner</code> may be null, but
 324      * if it is a visible Window, it will be used as the parent.
 325      * <p>
 326      * This method may be called from any thread. If it is called from the FX
 327      * application thread, then it must either be called from an input event
 328      * handler or from the run method of a Runnable passed to
 329      * {@link javafx.application.Platform#runLater Platform.runLater}.
 330      * It must not be called during animation or layout processing.
 331      *
 332      * @param owner to block input, or null.
 333      * @return false if the user opts to cancel the dialog, or the job
 334      * is not in the new state. That is if it has already started,
 335      * has failed, or has been cancelled, or ended.
 336      * @throws IllegalStateException if this method is called during
 337      * animation or layout processing.
 338      */
 339     public synchronized boolean showPageSetupDialog(Window owner) {
 340         // TBD handle owner
 341         if (!isJobNew()) {
 342             return false;
 343         } else {
 344             return jobImpl.showPageDialog(owner);
 345         }
 346     }
 347 
 348     /**
 349      * This method can be used to check if a page configuration
 350      * is possible in the current job configuration. For example
 351      * if the specified paper size is supported. If the original
 352      * PageLayout is supported it will be returned. If not, a new PageLayout
 353      * will be returned that attempts to honour the supplied
 354      * PageLayout, but adjusted to match the current job configuration.
 355      * <p>
 356      * This method does not update the job configuration.
 357      * @param pageLayout to be validated
 358      * @return a <code>PageLayout</code> that is supported in the
 359      * current job configuration.
 360      * @throws NullPointerException if the pageLayout parameter is null.
 361      */
 362     synchronized PageLayout validatePageLayout(PageLayout pageLayout) {
 363         if (pageLayout == null) {
 364             throw new NullPointerException("pageLayout cannot be null");
 365         }
 366         return jobImpl.validatePageLayout(pageLayout);
 367     }
 368 
 369     /**
 370      * Print the specified node using the specified page layout.
 371      * The page layout will override the job default for this page only.
 372      * If the job state is CANCELED, ERROR or DONE, this method will
 373      * return false.
 374      * <p>
 375      * This method may be called from any thread. If it is called from the FX
 376      * application thread, then it must either be called from an input event
 377      * handler or from the run method of a Runnable passed to
 378      * {@link javafx.application.Platform#runLater Platform.runLater}.
 379      * It must not be called during animation or layout processing.
 380      *
 381      * @param pageLayout Layout for this page.
 382      * @param node The node to print.
 383      * @return whether rendering was successful.
 384      * @throws NullPointerException if either parameter is null.
 385      * @throws IllegalStateException if this method is called during
 386      * animation or layout processing.
 387      */
 388     public synchronized boolean printPage(PageLayout pageLayout, Node node) {
 389         if (jobStatus.get().ordinal() > JobStatus.PRINTING.ordinal()) {
 390             return false;
 391         }
 392         if (jobStatus.get() == JobStatus.NOT_STARTED) {
 393             jobStatus.set(JobStatus.PRINTING);
 394         }
 395         if (pageLayout == null || node == null) {
 396             jobStatus.set(JobStatus.ERROR);
 397             throw new NullPointerException("Parameters cannot be null");
 398         }
 399         boolean rv = jobImpl.print(pageLayout, node);
 400         if (!rv) {
 401             jobStatus.set(JobStatus.ERROR);
 402         }
 403         return rv;
 404     }
 405 
 406      /**
 407      * Print the specified node. The page layout is the job default.
 408      * If the job state is CANCELED, ERROR or DONE, this method will
 409      * return false.
 410      * @param node The node to print.
 411      * @return whether rendering was successful.
 412      * @throws NullPointerException if the node parameter is null.
 413      */
 414     public synchronized boolean printPage(Node node) {
 415         return printPage(settings.getPageLayout(), node);
 416     }
 417 
 418     /**
 419      * An enum class used in reporting status of a print job.
 420      * Applications can listen to the job status via the
 421      * {@link #jobStatusProperty() jobStatus} property, or may query it directly
 422      * using {@link PrinterJob#getJobStatus() getJobStatus()}.
 423      * <p>
 424      * The typical life cycle of a job is as follows :
 425      * <ul>
 426      * <li>job will be created with status <code>NOT_STARTED</code> and
 427      * will stay there during configuration via dialogs etc.
 428      * <li>job will enter state <code>PRINTING</code> when the first page
 429      * is printed.
 430      * <li>job will enter state <code>DONE</code> once the job is
 431      * successfully completed without being cancelled or encountering
 432      * an error. The job is now completed.
 433      * <li>A job that encounters an <code>ERROR</code> or is
 434      * <code>CANCELED</code> is also considered completed.
 435      * </ul>
 436      * <p>
 437      * A job may not revert to an earlier status in its life cycle and
 438      * the current job state affects operations that may be performed.
 439      * For example a job may not begin printing again if it has previously
 440      * passed that state and entered any of the termination states.
 441      *
 442      * @since JavaFX 8.0
 443      */
 444     public static enum JobStatus {
 445 
 446         /**
 447          * The new job status. May display print dialogs and
 448          * configure the job and initiate printing.
 449          */
 450         NOT_STARTED,
 451 
 452         /**
 453          * The job has requested to print at least one page,
 454          * and has not terminated printing. May no longer
 455          * display print dialogs.
 456          */
 457         PRINTING,
 458 
 459         /**
 460          * The job has been cancelled by the application.
 461          * May not display dialogs or initiate printing.
 462          * Job should be discarded. There is no need to
 463          * call endJob().
 464          */
 465         CANCELED,
 466 
 467         /**
 468          * The job encountered an error.
 469          * Job should be discarded. There is no need to
 470          * call endJob().
 471          */
 472         ERROR,
 473 
 474         /**
 475          * The job initiated printing and later called endJob()
 476          * which reported success. The job can be discarded
 477          * as it cannot be re-used.
 478          */
 479         DONE
 480     };
 481 
 482     private ReadOnlyObjectWrapper<JobStatus> jobStatus =
 483         new ReadOnlyObjectWrapper(JobStatus.NOT_STARTED);
 484 
 485     /**
 486      * A read only object property representing the current
 487      * <code>JobStatus</code>
 488      * @return the current <code>JobStatus</code>
 489      */
 490     public ReadOnlyObjectProperty<JobStatus> jobStatusProperty() {
 491         return jobStatus.getReadOnlyProperty();
 492     }
 493 
 494     /**
 495      * Obtain the current status of the job.
 496      * @return the current <code>JobStatus</code>
 497      */
 498     public JobStatus getJobStatus() {
 499         return jobStatus.get();
 500     }
 501 
 502     /**
 503      * Cancel the underlying print job at the earliest opportunity.
 504      * It may return immediately without waiting for the job cancellation
 505      * to be complete in case this would block the FX user thread
 506      * for any period of time.
 507      * If printing is in process at that time, then typically
 508      * this means cancellation is after the current page is rendered.
 509      * The job status is updated to CANCELED only once that has happened.
 510      * Thus determining that the job is CANCELED requires monitoring
 511      * the job status.
 512      * <p>
 513      * The call has no effect if the job has already been requested
 514      * to be CANCELED, or is in the state ERROR or DONE.
 515      * For example it will not de-queue from the printer a job that
 516      * has already been spooled for printing.
 517      * Once a job is cancelled, it is not valid to call any methods
 518      * which render new content or change job state.
 519      */
 520     public void cancelJob() {
 521         if (jobStatus.get().ordinal() <= JobStatus.PRINTING.ordinal()) {
 522             jobStatus.set(JobStatus.CANCELED);
 523             jobImpl.cancelJob();
 524         }
 525     }
 526 
 527     /**
 528      * If the job can be successfully spooled to the printer queue
 529      * this will return true. Note : this does not mean the job already
 530      * printed as that could entail waiting for minutes or longer,
 531      * even where implementable.
 532      * <p>
 533      * A return value of false means the job could not be spooled,
 534      * or was already completed.
 535      * <p>
 536      * Successful completion will also update job status to <code>DONE</code>,
 537      * at which point the job can no longer be used.
 538      * <p>
 539      * Calling endJob() on a job for which no pages have been printed
 540      * is equivalent to calling {code cancelJob()}.
 541      * @return true if job is spooled, false if its not, or the job
 542      * was already in a completed state.
 543      */
 544     public synchronized boolean endJob() {
 545         if (jobStatus.get() == JobStatus.NOT_STARTED) {
 546             cancelJob();
 547             return false;
 548         } else if (jobStatus.get() == JobStatus.PRINTING) {
 549             boolean rv = jobImpl.endJob();
 550             jobStatus.set(rv ? JobStatus.DONE : JobStatus.ERROR);
 551             return rv;
 552         } else {
 553             return false;
 554         }
 555     }
 556 
 557     @Override
 558     public String toString() {
 559         return "JavaFX PrinterJob " +
 560             getPrinter() + "\n" +
 561             getJobSettings() + "\n" +
 562             "Job Status = " + getJobStatus();
 563     }
 564 
 565 }