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 #jobStatus jobStatus} property, or may query it directly 422 * using {@link javafx.print.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 }