From 1eb41d9bf0d0f540f2c0758ba2bb0502ac494073 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 3 Jul 2025 09:38:18 +0200 Subject: [PATCH 1/2] Option to put screenshot on clipboard --- .../org/phoebus/ui/application/Messages.java | 2 + .../ui/application/SaveSnapshotAction.java | 18 ++- .../org/phoebus/ui/javafx/Screenshot.java | 153 +++++++++++------- .../ui/application/messages.properties | 4 +- 4 files changed, 118 insertions(+), 59 deletions(-) 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..3b48082109 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,74 @@ 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 +110,84 @@ 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. + * + * @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 From a9a7e8b3027e7d560024ba69e0a9a63f2ae1f4c3 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 3 Jul 2025 11:23:18 +0200 Subject: [PATCH 2/2] Add clarifying Javadoc comment --- .../ui/src/main/java/org/phoebus/ui/javafx/Screenshot.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 3b48082109..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 @@ -71,7 +71,6 @@ public static BufferedImage bufferFromImage(final Image jfx) { (int) jfx.getHeight(), BufferedImage.TYPE_INT_ARGB); SwingFXUtils.fromFXImage(jfx, img); - return img; } @@ -153,6 +152,12 @@ public File writeToTempfile(final String file_prefix) throws Exception { /** * 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) {