diff --git a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java index cdd2c6cd49..4cc7451f2a 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java @@ -128,6 +128,8 @@ public class Messages public static String SaveLayoutWarningApplicationNoSaveFileTitle; public static String SaveSnapshot; public static String SaveSnapshotSelectFilename; + public static String SaveSnapshotToClipboard; + public static String SaveSnapshotToFile; public static String Saving; public static String SavingAlert; public static String SavingAlertTitle; diff --git a/core/ui/src/main/java/org/phoebus/ui/application/SaveSnapshotAction.java b/core/ui/src/main/java/org/phoebus/ui/application/SaveSnapshotAction.java index 8980e0cfa9..53df69cf0d 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/SaveSnapshotAction.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/SaveSnapshotAction.java @@ -9,6 +9,7 @@ import java.io.File; +import javafx.scene.control.Menu; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.dialog.SaveAsDialog; @@ -26,17 +27,28 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -public class SaveSnapshotAction extends MenuItem +public class SaveSnapshotAction extends Menu { private static final Image icon = ImageCache.getImage(SaveSnapshotAction.class, "/icons/save_edit.png"); + private static final Image copyIcon = ImageCache.getImage(SaveSnapshotAction.class, "/icons/copy.png"); private static final ExtensionFilter all_file_extensions = new ExtensionFilter(Messages.AllFiles, "*.*"); private static final ExtensionFilter image_file_extension = new ExtensionFilter(Messages.ImagePng, "*.png"); + /** @param node Node in scene of which to take snapshot */ public SaveSnapshotAction(final Node node) { - super(Messages.SaveSnapshot, new ImageView(icon)); - setOnAction(event -> save(node)); + setText(Messages.SaveSnapshot); + setGraphic(new ImageView(icon)); + + MenuItem saveToFileMenuItem = new MenuItem(Messages.SaveSnapshotToFile, new ImageView(icon)); + saveToFileMenuItem.setOnAction(e -> save(node)); + + MenuItem copyToClipboard = new MenuItem(Messages.SaveSnapshotToClipboard, new ImageView(copyIcon)); + copyToClipboard.setOnAction(e -> Screenshot.copyToClipboard(node)); + + getItems().addAll(saveToFileMenuItem, copyToClipboard); + } /** @param node Node of which to save a snapshot */ diff --git a/core/ui/src/main/java/org/phoebus/ui/javafx/Screenshot.java b/core/ui/src/main/java/org/phoebus/ui/javafx/Screenshot.java index 5607c2227a..211c082c7c 100644 --- a/core/ui/src/main/java/org/phoebus/ui/javafx/Screenshot.java +++ b/core/ui/src/main/java/org/phoebus/ui/javafx/Screenshot.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2017 Oak Ridge National Laboratory. + * Copyright (c) 2015-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,11 +7,6 @@ *******************************************************************************/ package org.phoebus.ui.javafx; -import java.awt.image.BufferedImage; -import java.io.File; - -import javax.imageio.ImageIO; - import javafx.embed.swing.SwingFXUtils; import javafx.scene.Node; import javafx.scene.Scene; @@ -19,63 +14,73 @@ import javafx.scene.image.WritableImage; import javafx.scene.input.Clipboard; -/** Create screenshot of a JavaFX scene - * @author Kay Kasemir +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.image.BufferedImage; +import java.io.File; + +/** + * Create screenshot of a JavaFX scene + * + * @author Kay Kasemir */ @SuppressWarnings("nls") -public class Screenshot -{ +public class Screenshot { private final BufferedImage image; - /** Initialize screenshot + /** + * Initialize screenshot + * + *
Must be called on UI thread * - *
Must be called on UI thread - * @param scene Scene to capture + * @param scene Scene to capture */ - public Screenshot(final Scene scene) - { + public Screenshot(final Scene scene) { image = bufferFromNode(scene.getRoot()); } - public Screenshot(final Node node) - { + public Screenshot(final Node node) { image = bufferFromNode(node); } - public Screenshot(final Image image) - { + public Screenshot(final Image image) { this.image = bufferFromImage(image); } - /** Get a JavaFX Node Snapshot as a JavaFX Image + /** + * Get a JavaFX Node Snapshot as a JavaFX Image + * * @param node * @return Image */ - public static WritableImage imageFromNode(Node node) - { + public static WritableImage imageFromNode(Node node) { return node.snapshot(null, null); } - /** Get a AWT BufferedImage from JavaFX Image - * @param jfx {@link Image} - * @return BufferedImage + /** + * Get a AWT BufferedImage from JavaFX Image + * + * @param jfx {@link Image} + * @return BufferedImage */ - public static BufferedImage bufferFromImage(final Image jfx) - { - final BufferedImage img = new BufferedImage((int)jfx.getWidth(), - (int)jfx.getHeight(), + public static BufferedImage bufferFromImage(final Image jfx) { + final BufferedImage img = new BufferedImage((int) jfx.getWidth(), + (int) jfx.getHeight(), BufferedImage.TYPE_INT_ARGB); SwingFXUtils.fromFXImage(jfx, img); - return img; } - /** Get a JavaFX Node Snapshot as an AWT BufferedImage + /** + * Get a JavaFX Node Snapshot as an AWT BufferedImage + * * @param node * @return BufferedImage */ - public static BufferedImage bufferFromNode(Node node) - { + public static BufferedImage bufferFromNode(Node node) { return bufferFromImage(imageFromNode(node)); } @@ -104,47 +109,90 @@ public static Image captureScreen() /** * Get an image from the clip board. *
Returns null if no image is on the clip board. + * * @return Image */ - public static Image getImageFromClipboard() - { + public static Image getImageFromClipboard() { Clipboard clipboard = Clipboard.getSystemClipboard(); return clipboard.getImage(); } - /** Write to file - * @param file Output file - * @throws Exception on error + /** + * Write to file + * + * @param file Output file + * @throws Exception on error */ - public void writeToFile(final File file) throws Exception - { - try - { + public void writeToFile(final File file) throws Exception { + try { ImageIO.write(image, "png", file); - } - catch (Exception ex) - { + } catch (Exception ex) { throw new Exception("Cannot create screenshot " + file.getAbsolutePath(), ex); } } - /** Write to temp. file - * @param file_prefix File prefix - * @return File that was created - * @throws Exception on error + /** + * Write to temp. file + * + * @param file_prefix File prefix + * @return File that was created + * @throws Exception on error */ - public File writeToTempfile(final String file_prefix) throws Exception - { - try - { + public File writeToTempfile(final String file_prefix) throws Exception { + try { final File file = File.createTempFile(file_prefix, ".png"); file.deleteOnExit(); writeToFile(file); return file; - } - catch (Exception ex) - { + } catch (Exception ex) { throw new Exception("Cannot create tmp. file:\n" + ex.getMessage()); } } + + /** + * Puts the {@link Node} as image data onto the clipboard. + * + *
+ * NOTE: on Windows calling this will throw an {@link java.io.IOException}, but screenshot will still be available on + * the clipboard. This Stackoverflow post + * suggests the printed stack trace is in fact debug information. + *
+ * + * @param node Node from which to take a screenshot. + */ + public static void copyToClipboard(Node node) { + BufferedImage bufferedImage = bufferFromNode(node); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new TransferableImage(bufferedImage), null); + } + + /** + * Minimal implementation to support putting image data on the clipboard + */ + private static class TransferableImage implements Transferable { + + private final java.awt.Image image; + + public TransferableImage(java.awt.Image image) { + this.image = image; + } + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { + if (flavor.equals(DataFlavor.imageFlavor) && image != null) { + return image; + } else { + throw new UnsupportedFlavorException(flavor); + } + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[]{DataFlavor.imageFlavor}; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavor.equals(DataFlavor.imageFlavor); + } + } } diff --git a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties index 1ef7651dfb..5582505f2a 100644 --- a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties +++ b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties @@ -112,8 +112,10 @@ SaveLayoutAs=Save Layout As... SaveLayoutOfContainingWindowAs=Save Layout of Containing Window As... SaveLayoutWarningApplicationNoSaveFile=The following application(s) do not have associated with them save file(s). No save file association(s) will be stored for the application instance(s) in question when proceeding to save the layout. Proceed?\n\nThe application(s) in question are:\n SaveLayoutWarningApplicationNoSaveFileTitle=Warning: application(s) are not associated with save file(s) -SaveSnapshot=Save Screenshot... +SaveSnapshot=Take Screenshot SaveSnapshotSelectFilename=Enter *.png name for screenshot +SaveSnapshotToClipboard=Copy to clipboard +SaveSnapshotToFile=Save to file Saving=Saving {0}... SavingAlert=The file\n {0}\nis read-only.\n\nSave to a different file? SavingAlertTitle=File has changed