Skip to content

Commit 32806b7

Browse files
authored
Merge pull request #3454 from ControlSystemStudio/CSSTUDIO-3313
Add option to copy screenshot to clipboard
2 parents 222c875 + a9a7e8b commit 32806b7

File tree

4 files changed

+124
-60
lines changed

4 files changed

+124
-60
lines changed

core/ui/src/main/java/org/phoebus/ui/application/Messages.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ public class Messages
128128
public static String SaveLayoutWarningApplicationNoSaveFileTitle;
129129
public static String SaveSnapshot;
130130
public static String SaveSnapshotSelectFilename;
131+
public static String SaveSnapshotToClipboard;
132+
public static String SaveSnapshotToFile;
131133
public static String Saving;
132134
public static String SavingAlert;
133135
public static String SavingAlertTitle;

core/ui/src/main/java/org/phoebus/ui/application/SaveSnapshotAction.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.io.File;
1111

12+
import javafx.scene.control.Menu;
1213
import org.phoebus.framework.jobs.JobManager;
1314
import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog;
1415
import org.phoebus.ui.dialog.SaveAsDialog;
@@ -26,17 +27,28 @@
2627
* @author Kay Kasemir
2728
*/
2829
@SuppressWarnings("nls")
29-
public class SaveSnapshotAction extends MenuItem
30+
public class SaveSnapshotAction extends Menu
3031
{
3132
private static final Image icon = ImageCache.getImage(SaveSnapshotAction.class, "/icons/save_edit.png");
33+
private static final Image copyIcon = ImageCache.getImage(SaveSnapshotAction.class, "/icons/copy.png");
3234
private static final ExtensionFilter all_file_extensions = new ExtensionFilter(Messages.AllFiles, "*.*");
3335
private static final ExtensionFilter image_file_extension = new ExtensionFilter(Messages.ImagePng, "*.png");
3436

37+
3538
/** @param node Node in scene of which to take snapshot */
3639
public SaveSnapshotAction(final Node node)
3740
{
38-
super(Messages.SaveSnapshot, new ImageView(icon));
39-
setOnAction(event -> save(node));
41+
setText(Messages.SaveSnapshot);
42+
setGraphic(new ImageView(icon));
43+
44+
MenuItem saveToFileMenuItem = new MenuItem(Messages.SaveSnapshotToFile, new ImageView(icon));
45+
saveToFileMenuItem.setOnAction(e -> save(node));
46+
47+
MenuItem copyToClipboard = new MenuItem(Messages.SaveSnapshotToClipboard, new ImageView(copyIcon));
48+
copyToClipboard.setOnAction(e -> Screenshot.copyToClipboard(node));
49+
50+
getItems().addAll(saveToFileMenuItem, copyToClipboard);
51+
4052
}
4153

4254
/** @param node Node of which to save a snapshot */
Lines changed: 104 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,86 @@
11
/*******************************************************************************
2-
* Copyright (c) 2015-2017 Oak Ridge National Laboratory.
2+
* Copyright (c) 2015-2025 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
66
* http://www.eclipse.org/legal/epl-v10.html
77
*******************************************************************************/
88
package org.phoebus.ui.javafx;
99

10-
import java.awt.image.BufferedImage;
11-
import java.io.File;
12-
13-
import javax.imageio.ImageIO;
14-
1510
import javafx.embed.swing.SwingFXUtils;
1611
import javafx.scene.Node;
1712
import javafx.scene.Scene;
1813
import javafx.scene.image.Image;
1914
import javafx.scene.image.WritableImage;
2015
import javafx.scene.input.Clipboard;
2116

22-
/** Create screenshot of a JavaFX scene
23-
* @author Kay Kasemir
17+
import javax.imageio.ImageIO;
18+
import java.awt.*;
19+
import java.awt.datatransfer.DataFlavor;
20+
import java.awt.datatransfer.Transferable;
21+
import java.awt.datatransfer.UnsupportedFlavorException;
22+
import java.awt.image.BufferedImage;
23+
import java.io.File;
24+
25+
/**
26+
* Create screenshot of a JavaFX scene
27+
*
28+
* @author Kay Kasemir
2429
*/
2530
@SuppressWarnings("nls")
26-
public class Screenshot
27-
{
31+
public class Screenshot {
2832
private final BufferedImage image;
2933

30-
/** Initialize screenshot
34+
/**
35+
* Initialize screenshot
36+
*
37+
* <p>Must be called on UI thread
3138
*
32-
* <p>Must be called on UI thread
33-
* @param scene Scene to capture
39+
* @param scene Scene to capture
3440
*/
35-
public Screenshot(final Scene scene)
36-
{
41+
public Screenshot(final Scene scene) {
3742
image = bufferFromNode(scene.getRoot());
3843
}
3944

40-
public Screenshot(final Node node)
41-
{
45+
public Screenshot(final Node node) {
4246
image = bufferFromNode(node);
4347
}
4448

45-
public Screenshot(final Image image)
46-
{
49+
public Screenshot(final Image image) {
4750
this.image = bufferFromImage(image);
4851
}
4952

50-
/** Get a JavaFX Node Snapshot as a JavaFX Image
53+
/**
54+
* Get a JavaFX Node Snapshot as a JavaFX Image
55+
*
5156
* @param node
5257
* @return Image
5358
*/
54-
public static WritableImage imageFromNode(Node node)
55-
{
59+
public static WritableImage imageFromNode(Node node) {
5660
return node.snapshot(null, null);
5761
}
5862

59-
/** Get a AWT BufferedImage from JavaFX Image
60-
* @param jfx {@link Image}
61-
* @return BufferedImage
63+
/**
64+
* Get a AWT BufferedImage from JavaFX Image
65+
*
66+
* @param jfx {@link Image}
67+
* @return BufferedImage
6268
*/
63-
public static BufferedImage bufferFromImage(final Image jfx)
64-
{
65-
final BufferedImage img = new BufferedImage((int)jfx.getWidth(),
66-
(int)jfx.getHeight(),
69+
public static BufferedImage bufferFromImage(final Image jfx) {
70+
final BufferedImage img = new BufferedImage((int) jfx.getWidth(),
71+
(int) jfx.getHeight(),
6772
BufferedImage.TYPE_INT_ARGB);
6873
SwingFXUtils.fromFXImage(jfx, img);
69-
7074
return img;
7175
}
7276

73-
/** Get a JavaFX Node Snapshot as an AWT BufferedImage
77+
/**
78+
* Get a JavaFX Node Snapshot as an AWT BufferedImage
79+
*
7480
* @param node
7581
* @return BufferedImage
7682
*/
77-
public static BufferedImage bufferFromNode(Node node)
78-
{
83+
public static BufferedImage bufferFromNode(Node node) {
7984
return bufferFromImage(imageFromNode(node));
8085
}
8186

@@ -104,47 +109,90 @@ public static Image captureScreen()
104109
/**
105110
* Get an image from the clip board.
106111
* <p> Returns null if no image is on the clip board.
112+
*
107113
* @return Image
108114
*/
109-
public static Image getImageFromClipboard()
110-
{
115+
public static Image getImageFromClipboard() {
111116
Clipboard clipboard = Clipboard.getSystemClipboard();
112117
return clipboard.getImage();
113118
}
114119

115-
/** Write to file
116-
* @param file Output file
117-
* @throws Exception on error
120+
/**
121+
* Write to file
122+
*
123+
* @param file Output file
124+
* @throws Exception on error
118125
*/
119-
public void writeToFile(final File file) throws Exception
120-
{
121-
try
122-
{
126+
public void writeToFile(final File file) throws Exception {
127+
try {
123128
ImageIO.write(image, "png", file);
124-
}
125-
catch (Exception ex)
126-
{
129+
} catch (Exception ex) {
127130
throw new Exception("Cannot create screenshot " + file.getAbsolutePath(), ex);
128131
}
129132
}
130133

131-
/** Write to temp. file
132-
* @param file_prefix File prefix
133-
* @return File that was created
134-
* @throws Exception on error
134+
/**
135+
* Write to temp. file
136+
*
137+
* @param file_prefix File prefix
138+
* @return File that was created
139+
* @throws Exception on error
135140
*/
136-
public File writeToTempfile(final String file_prefix) throws Exception
137-
{
138-
try
139-
{
141+
public File writeToTempfile(final String file_prefix) throws Exception {
142+
try {
140143
final File file = File.createTempFile(file_prefix, ".png");
141144
file.deleteOnExit();
142145
writeToFile(file);
143146
return file;
144-
}
145-
catch (Exception ex)
146-
{
147+
} catch (Exception ex) {
147148
throw new Exception("Cannot create tmp. file:\n" + ex.getMessage());
148149
}
149150
}
151+
152+
/**
153+
* Puts the {@link Node} as image data onto the clipboard.
154+
*
155+
* <p>
156+
* <b>NOTE:</b> on Windows calling this will throw an {@link java.io.IOException}, but screenshot will still be available on
157+
* the clipboard. <a href='https://stackoverflow.com/questions/59140881/error-copying-an-image-object-to-the-clipboard'>This Stackoverflow post</a>
158+
* suggests the printed stack trace is in fact debug information.
159+
* </p>
160+
*
161+
* @param node Node from which to take a screenshot.
162+
*/
163+
public static void copyToClipboard(Node node) {
164+
BufferedImage bufferedImage = bufferFromNode(node);
165+
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new TransferableImage(bufferedImage), null);
166+
}
167+
168+
/**
169+
* Minimal implementation to support putting image data on the clipboard
170+
*/
171+
private static class TransferableImage implements Transferable {
172+
173+
private final java.awt.Image image;
174+
175+
public TransferableImage(java.awt.Image image) {
176+
this.image = image;
177+
}
178+
179+
@Override
180+
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
181+
if (flavor.equals(DataFlavor.imageFlavor) && image != null) {
182+
return image;
183+
} else {
184+
throw new UnsupportedFlavorException(flavor);
185+
}
186+
}
187+
188+
@Override
189+
public DataFlavor[] getTransferDataFlavors() {
190+
return new DataFlavor[]{DataFlavor.imageFlavor};
191+
}
192+
193+
@Override
194+
public boolean isDataFlavorSupported(DataFlavor flavor) {
195+
return flavor.equals(DataFlavor.imageFlavor);
196+
}
197+
}
150198
}

core/ui/src/main/resources/org/phoebus/ui/application/messages.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ SaveLayoutAs=Save Layout As...
112112
SaveLayoutOfContainingWindowAs=Save Layout of Containing Window As...
113113
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
114114
SaveLayoutWarningApplicationNoSaveFileTitle=Warning: application(s) are not associated with save file(s)
115-
SaveSnapshot=Save Screenshot...
115+
SaveSnapshot=Take Screenshot
116116
SaveSnapshotSelectFilename=Enter *.png name for screenshot
117+
SaveSnapshotToClipboard=Copy to clipboard
118+
SaveSnapshotToFile=Save to file
117119
Saving=Saving {0}...
118120
SavingAlert=The file\n {0}\nis read-only.\n\nSave to a different file?
119121
SavingAlertTitle=File has changed

0 commit comments

Comments
 (0)