36 import javafx.scene.input.KeyCode;
37 import javafx.scene.input.KeyEvent;
38 import javafx.scene.input.MouseButton;
39 import javafx.scene.input.MouseEvent;
40 import javafx.scene.input.ScrollEvent;
41 import javafx.stage.Window;
42 import javax.swing.JComponent;
43 import javax.swing.Timer;
44 import java.awt.AWTEvent;
45 import java.awt.Component;
46 import java.awt.Cursor;
47 import java.awt.EventQueue;
48 import java.awt.Toolkit;
49 import java.lang.ref.WeakReference;
50 import java.awt.dnd.DragGestureEvent;
51 import java.awt.dnd.DragGestureListener;
52 import java.awt.dnd.DragGestureRecognizer;
53 import java.awt.dnd.DragSource;
54 import java.awt.dnd.DropTarget;
55 import java.awt.dnd.InvalidDnDOperationException;
56 import java.awt.dnd.peer.DragSourceContextPeer;
57 import java.awt.event.InputEvent;
58 import java.awt.event.MouseWheelEvent;
59 import java.awt.event.WindowEvent;
60 import java.awt.event.WindowFocusListener;
61 import java.lang.reflect.Method;
62 import java.nio.IntBuffer;
63 import java.security.AccessController;
64 import java.security.PrivilegedAction;
65 import java.util.ArrayList;
66 import java.util.HashSet;
67 import java.util.List;
68 import java.util.Set;
69 import java.util.concurrent.locks.ReentrantLock;
70 import com.sun.javafx.embed.swing.Disposer;
71 import com.sun.javafx.embed.swing.DisposerRecord;
72 import com.sun.javafx.geom.BaseBounds;
73 import com.sun.javafx.geom.transform.BaseTransform;
74 import com.sun.javafx.scene.DirtyBits;
75 import com.sun.javafx.sg.prism.NGExternalNode;
76 import com.sun.javafx.sg.prism.NGNode;
77 import com.sun.javafx.stage.FocusUngrabEvent;
78 import com.sun.javafx.stage.WindowHelper;
79 import com.sun.javafx.tk.TKStage;
80 import com.sun.javafx.PlatformUtil;
81 import com.sun.javafx.embed.swing.SwingNodeHelper;
82 import com.sun.javafx.scene.NodeHelper;
83 import sun.awt.UngrabEvent;
84 import sun.swing.JLightweightFrame;
85 import sun.swing.LightweightContent;
86
87 import static javafx.stage.WindowEvent.WINDOW_HIDDEN;
88
89 /**
90 * This class is used to embed a Swing content into a JavaFX application.
91 * The content to be displayed is specified with the {@link #setContent} method
92 * that accepts an instance of Swing {@code JComponent}. The hierarchy of components
93 * contained in the {@code JComponent} instance should not contain any heavyweight
94 * components, otherwise {@code SwingNode} may fail to paint it. The content gets
95 * repainted automatically. All the input and focus events are forwarded to the
96 * {@code JComponent} instance transparently to the developer.
97 * <p>
98 * Here is a typical pattern which demonstrates how {@code SwingNode} can be used:
99 * <pre>
100 * public class SwingFx extends Application {
101 *
102 * @Override
103 * public void start(Stage stage) {
104 * final SwingNode swingNode = new SwingNode();
105 * createAndSetSwingContent(swingNode);
159 }
160
161 @Override
162 public boolean doComputeContains(Node node, double localX, double localY) {
163 return ((SwingNode) node).doComputeContains(localX, localY);
164 }
165 });
166 }
167
168 private double fxWidth;
169 private double fxHeight;
170
171 private int swingPrefWidth;
172 private int swingPrefHeight;
173 private int swingMaxWidth;
174 private int swingMaxHeight;
175 private int swingMinWidth;
176 private int swingMinHeight;
177
178 private volatile JComponent content;
179 private volatile JLightweightFrame lwFrame;
180 final JLightweightFrame getLightweightFrame() { return lwFrame; }
181
182 private NGExternalNode peer;
183
184 private final ReentrantLock paintLock = new ReentrantLock();
185
186 private boolean skipBackwardUnrgabNotification;
187 private boolean grabbed; // lwframe initiated grab
188 private Timer deactivate; // lwFrame deactivate delay for Linux
189
190 {
191 // To initialize the class helper at the begining each constructor of this class
192 SwingNodeHelper.initHelper(this);
193 }
194
195 /**
196 * Constructs a new instance of {@code SwingNode}.
197 */
198 public SwingNode() {
199 setFocusTraversable(true);
200 setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler());
314 }
315
316 public boolean isIntegerApi() {
317 return isIntegerAPI;
318 }
319
320 public Object invoke(T object, Object... args) {
321 if (method != null) {
322 try {
323 return method.invoke(object, args);
324 } catch (Throwable ex) {
325 throw new RuntimeException("Error when calling " + object.getClass().getName() + "." + method.getName() + "().", ex);
326 }
327 } else {
328 return null;
329 }
330 }
331 }
332
333 /**
334 * Calls JLightweightFrame.notifyDisplayChanged.
335 * Must be called on EDT only.
336 */
337 private static OptionalMethod<JLightweightFrame> jlfNotifyDisplayChanged;
338 private static OptionalMethod<JLightweightFrame> jlfOverrideNativeWindowHandle;
339
340 static {
341 jlfNotifyDisplayChanged = new OptionalMethod<>(JLightweightFrame.class,
342 "notifyDisplayChanged", Double.TYPE, Double.TYPE);
343 if (!jlfNotifyDisplayChanged.isSupported()) {
344 jlfNotifyDisplayChanged = new OptionalMethod<>(
345 JLightweightFrame.class,"notifyDisplayChanged", Integer.TYPE);
346 }
347
348 jlfOverrideNativeWindowHandle = new OptionalMethod<>(JLightweightFrame.class,
349 "overrideNativeWindowHandle", Long.TYPE, Runnable.class);
350
351 }
352
353 /*
354 * Called on EDT
355 */
356 private void setContentImpl(JComponent content) {
357 if (lwFrame != null) {
358 lwFrame.dispose();
359 lwFrame = null;
360 }
361 if (content != null) {
362 lwFrame = new JLightweightFrame();
363
364 SwingNodeWindowFocusListener snfListener =
365 new SwingNodeWindowFocusListener(this);
366 lwFrame.addWindowFocusListener(snfListener);
367
368 if (getScene() != null) {
369 Window window = getScene().getWindow();
370 if (window != null) {
371 if (jlfNotifyDisplayChanged.isIntegerApi()) {
372 jlfNotifyDisplayChanged.invoke(lwFrame,
373 (int) Math.round(window.getRenderScaleX()));
374 } else {
375 jlfNotifyDisplayChanged.invoke(lwFrame,
376 window.getRenderScaleX(),
377 window.getRenderScaleY());
378 }
379 }
380 }
381 lwFrame.setContent(new SwingNodeContent(content, this));
382 lwFrame.setVisible(true);
542 * @return the minimum height that the node should be resized to during layout
543 */
544 @Override public double minHeight(double width) {
545 return swingMinHeight;
546 }
547
548 /*
549 * Note: This method MUST only be called via its accessor method.
550 */
551 private boolean doComputeContains(double localX, double localY) {
552 return true;
553 }
554
555 private final InvalidationListener locationListener = observable -> {
556 locateLwFrame();
557 };
558
559 private final EventHandler<FocusUngrabEvent> ungrabHandler = event -> {
560 if (!skipBackwardUnrgabNotification) {
561 if (lwFrame != null) {
562 AccessController.doPrivileged(new PostEventAction(new UngrabEvent(lwFrame)));
563 }
564 }
565 };
566
567 private final ChangeListener<Boolean> windowVisibleListener = (observable, oldValue, newValue) -> {
568 if (!newValue) {
569 disposeLwFrame();
570 } else {
571 setContent(content);
572 }
573 };
574
575 private final ChangeListener<Window> sceneWindowListener = (observable, oldValue, newValue) -> {
576 if (oldValue != null) {
577 removeWindowListeners(oldValue);
578 }
579
580 notifyNativeHandle(newValue);
581
582 if (newValue != null) {
656 setLwFrameVisible(newValue);
657 });
658
659 return peer;
660 }
661
662 /*
663 * Note: This method MUST only be called via its accessor method.
664 */
665 private void doUpdatePeer() {
666 if (NodeHelper.isDirty(this, DirtyBits.NODE_VISIBLE)
667 || NodeHelper.isDirty(this, DirtyBits.NODE_BOUNDS)) {
668 locateLwFrame(); // initialize location
669 }
670 if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
671 peer.markContentDirty();
672 }
673 }
674
675 /**
676 * Calls JLightweightFrame.setHostBounds.
677 * Must be called on EDT only.
678 */
679 private static final OptionalMethod<JLightweightFrame> jlfSetHostBounds =
680 new OptionalMethod<>(JLightweightFrame.class, "setHostBounds",
681 Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE);
682
683 private void locateLwFrame() {
684 if (getScene() == null
685 || lwFrame == null
686 || getScene().getWindow() == null
687 || !getScene().getWindow().isShowing()) {
688 // Not initialized yet. Skip the update to set the real values later
689 return;
690 }
691 Window w = getScene().getWindow();
692 double renderScaleX = w.getRenderScaleX();
693 double renderScaleY = w.getRenderScaleY();
694 final Point2D loc = localToScene(0, 0);
695 final int windowX = (int) (w.getX());
696 final int windowY = (int) (w.getY());
697 final int windowW = (int) (w.getWidth());
698 final int windowH = (int) (w.getHeight());
699 final int frameX = (int) Math.round(w.getX() + getScene().getX() + loc.getX());
700 final int frameY = (int) Math.round(w.getY() + getScene().getY() + loc.getY());
780 if (jlfNotifyDisplayChanged.isIntegerApi()) {
781 jlfNotifyDisplayChanged.invoke(lwFrame,
782 (int)Math.round(scaleX));
783 } else {
784 jlfNotifyDisplayChanged.invoke(lwFrame, scaleX, scaleY);
785 }
786 }
787 });
788 }
789
790 /*
791 * Note: This method MUST only be called via its accessor method.
792 */
793 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
794 bounds.deriveWithNewBounds(0, 0, 0, (float)fxWidth, (float)fxHeight, 0);
795 tx.transform(bounds, bounds);
796 return bounds;
797 }
798
799 private static class SwingNodeDisposer implements DisposerRecord {
800 JLightweightFrame lwFrame;
801
802 SwingNodeDisposer(JLightweightFrame ref) {
803 this.lwFrame = ref;
804 }
805 public void dispose() {
806 if (lwFrame != null) {
807 lwFrame.dispose();
808 lwFrame = null;
809 }
810 }
811 }
812
813 private static class SwingNodeWindowFocusListener implements WindowFocusListener {
814 private WeakReference<SwingNode> swingNodeRef;
815
816 SwingNodeWindowFocusListener(SwingNode swingNode) {
817 this.swingNodeRef = new WeakReference<SwingNode>(swingNode);
818 }
819
820 @Override
821 public void windowGainedFocus(WindowEvent e) {
822 SwingFXUtils.runOnFxThread(() -> {
823 SwingNode swingNode = swingNodeRef.get();
824 if (swingNode != null) {
825 swingNode.requestFocus();
826 }
827 });
828 }
829
830 @Override
831 public void windowLostFocus(WindowEvent e) {
832 SwingFXUtils.runOnFxThread(() -> {
833 SwingNode swingNode = swingNodeRef.get();
834 if (swingNode != null) {
835 swingNode.ungrabFocus(true);
836 }
837 });
838 }
839 }
840
841 private static class SwingNodeContent implements LightweightContent {
842 private JComponent comp;
843 private volatile FXDnD dnd;
844 private WeakReference<SwingNode> swingNodeRef;
845
846 SwingNodeContent(JComponent comp, SwingNode swingNode) {
847 this.comp = comp;
848 this.swingNodeRef = new WeakReference<SwingNode>(swingNode);
849 }
850 @Override
851 public JComponent getComponent() {
852 return comp;
853 }
854 @Override
855 public void paintLock() {
856 SwingNode swingNode = swingNodeRef.get();
857 if (swingNode != null) {
858 swingNode.paintLock.lock();
859 }
860 }
861 @Override
862 public void paintUnlock() {
863 SwingNode swingNode = swingNodeRef.get();
864 if (swingNode != null) {
865 swingNode.paintLock.unlock();
866 }
867 }
868
869 // Note: we skip @Override annotation and implement both pre-hiDPI and post-hiDPI versions
870 // of the method for compatibility.
871 //@Override
872 public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride) {
873 imageBufferReset(data, x, y, width, height, linestride, 1);
874 }
875 //@Override
876 public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride, int scale) {
877 SwingNode swingNode = swingNodeRef.get();
878 if (swingNode != null) {
879 swingNode.setImageBuffer(data, x, y, width, height, linestride, scale, scale);
880 }
881 }
882 //@Override
883 public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride, double scaleX, double scaleY) {
884 SwingNode swingNode = swingNodeRef.get();
885 if (swingNode != null) {
886 swingNode.setImageBuffer(data, x, y, width, height, linestride, scaleX, scaleY);
887 }
888 }
889 @Override
890 public void imageReshaped(int x, int y, int width, int height) {
891 SwingNode swingNode = swingNodeRef.get();
892 if (swingNode != null) {
893 swingNode.setImageBounds(x, y, width, height);
894 }
895 }
896 @Override
897 public void imageUpdated(int dirtyX, int dirtyY, int dirtyWidth, int dirtyHeight) {
898 SwingNode swingNode = swingNodeRef.get();
899 if (swingNode != null) {
900 swingNode.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
901 }
902 }
967 SwingFXUtils.runOnFxThread(() -> {
968 SwingNode swingNode = swingNodeRef.get();
969 if (swingNode != null) {
970 swingNode.setCursor(SwingCursors.embedCursorToCursor(cursor));
971 }
972 });
973 }
974
975 private void initDnD() {
976 // This is a part of AWT API, so the method may be invoked on any thread
977 synchronized (SwingNodeContent.this) {
978 if (this.dnd == null) {
979 SwingNode swingNode = swingNodeRef.get();
980 if (swingNode != null) {
981 this.dnd = new FXDnD(swingNode);
982 }
983 }
984 }
985 }
986
987 //@Override
988 public synchronized <T extends DragGestureRecognizer> T createDragGestureRecognizer(
989 Class<T> abstractRecognizerClass,
990 DragSource ds, Component c, int srcActions,
991 DragGestureListener dgl)
992 {
993 initDnD();
994 return dnd.createDragGestureRecognizer(abstractRecognizerClass, ds, c, srcActions, dgl);
995 }
996
997 //@Override
998 public DragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException
999 {
1000 initDnD();
1001 return dnd.createDragSourceContextPeer(dge);
1002 }
1003
1004 //@Override
1005 public void addDropTarget(DropTarget dt) {
1006 initDnD();
1007 dnd.addDropTarget(dt);
1008 }
1009
1010 //@Override
1011 public void removeDropTarget(DropTarget dt) {
1012 initDnD();
1013 dnd.removeDropTarget(dt);
1014 }
1015 }
1016
1017 private void ungrabFocus(boolean postUngrabEvent) {
1018 // On X11 grab is limited to a single XDisplay connection,
1019 // so we can't delegate it to another GUI toolkit.
1020 if (PlatformUtil.isLinux()) return;
1021
1022 if (grabbed &&
1023 getScene() != null &&
1024 getScene().getWindow() != null &&
1025 WindowHelper.getPeer(getScene().getWindow()) != null)
1026 {
1027 skipBackwardUnrgabNotification = !postUngrabEvent;
1028 WindowHelper.getPeer(getScene().getWindow()).ungrabFocus();
1029 skipBackwardUnrgabNotification = false;
1030 grabbed = false;
1032 }
1033
1034 private class PostEventAction implements PrivilegedAction<Void> {
1035 private AWTEvent event;
1036 PostEventAction(AWTEvent event) {
1037 this.event = event;
1038 }
1039 @Override
1040 public Void run() {
1041 EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue();
1042 eq.postEvent(event);
1043 return null;
1044 }
1045 }
1046
1047 private class SwingMouseEventHandler implements EventHandler<MouseEvent> {
1048 private final Set<MouseButton> mouseClickedAllowed = new HashSet<>();
1049
1050 @Override
1051 public void handle(MouseEvent event) {
1052 JLightweightFrame frame = lwFrame;
1053 if (frame == null) {
1054 return;
1055 }
1056 int swingID = SwingEvents.fxMouseEventTypeToMouseID(event);
1057 if (swingID < 0) {
1058 return;
1059 }
1060
1061 // Prevent ancestors of the SwingNode from stealing the focus
1062 event.consume();
1063
1064 final EventType<?> type = event.getEventType();
1065 if (type == MouseEvent.MOUSE_PRESSED) {
1066 mouseClickedAllowed.add(event.getButton());
1067 } else if (type == MouseEvent.MOUSE_RELEASED) {
1068 // RELEASED comes before CLICKED, so we don't remove the button from the set
1069 //mouseClickedAllowed.remove(event.getButton());
1070 } else if (type == MouseEvent.MOUSE_DRAGGED) {
1071 // This is what AWT/Swing do
1072 mouseClickedAllowed.clear();
1073 } else if (type == MouseEvent.MOUSE_CLICKED) {
1074 if (event.getClickCount() == 1 && !mouseClickedAllowed.contains(event.getButton())) {
1075 // RT-34610: In FX, CLICKED events are generated even after dragging the mouse pointer
1076 // Note that this is only relevant for single clicks. Double clicks use a smudge factor.
1077 return;
1078 }
1079 mouseClickedAllowed.remove(event.getButton());
1080 }
1081 int swingModifiers = SwingEvents.fxMouseModsToMouseMods(event);
1082 boolean swingPopupTrigger = event.isPopupTrigger();
1083 int swingButton = SwingEvents.fxMouseButtonToMouseButton(event);
1084 long swingWhen = System.currentTimeMillis();
1085 int relX = (int) Math.round(event.getX());
1086 int relY = (int) Math.round(event.getY());
1087 int absX = (int) Math.round(event.getScreenX());
1088 int absY = (int) Math.round(event.getScreenY());
1089 java.awt.event.MouseEvent mouseEvent =
1090 new java.awt.event.MouseEvent(
1091 frame, swingID, swingWhen, swingModifiers,
1092 relX, relY, absX, absY,
1093 event.getClickCount(), swingPopupTrigger, swingButton);
1094 AccessController.doPrivileged(new PostEventAction(mouseEvent));
1095 }
1096 }
1097
1098 private class SwingScrollEventHandler implements EventHandler<ScrollEvent> {
1099 @Override
1100 public void handle(ScrollEvent event) {
1101 JLightweightFrame frame = lwFrame;
1102 if (frame == null) {
1103 return;
1104 }
1105
1106 int swingModifiers = SwingEvents.fxScrollModsToMouseWheelMods(event);
1107 final boolean isShift = (swingModifiers & InputEvent.SHIFT_DOWN_MASK) != 0;
1108
1109 // Vertical scroll.
1110 if (!isShift && event.getDeltaY() != 0.0) {
1111 sendMouseWheelEvent(frame, event.getX(), event.getY(),
1112 swingModifiers, event.getDeltaY() / event.getMultiplierY());
1113 }
1114 // Horizontal scroll or shirt+vertical scroll.
1115 final double delta = isShift && event.getDeltaY() != 0.0
1116 ? event.getDeltaY() / event.getMultiplierY()
1117 : event.getDeltaX() / event.getMultiplierX();
1118 if (delta != 0.0) {
1119 swingModifiers |= InputEvent.SHIFT_DOWN_MASK;
1120 sendMouseWheelEvent(frame, event.getX(), event.getY(),
1121 swingModifiers, delta);
1122 }
1123 }
1124
1125 private void sendMouseWheelEvent(Component source, double fxX, double fxY, int swingModifiers, double delta) {
1126 int wheelRotation = (int) delta;
1127 int signum = (int) Math.signum(delta);
1128 if (signum * delta < 1) {
1129 wheelRotation = signum;
1130 }
1131 int x = (int) Math.round(fxX);
1132 int y = (int) Math.round(fxY);
1133 MouseWheelEvent mouseWheelEvent =
1134 new MouseWheelEvent(source, java.awt.event.MouseEvent.MOUSE_WHEEL,
1135 System.currentTimeMillis(), swingModifiers, x, y, 0, 0,
1136 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, 1 , -wheelRotation);
1137 AccessController.doPrivileged(new PostEventAction(mouseWheelEvent));
1138 }
1139 }
1140
1141 private class SwingKeyEventHandler implements EventHandler<KeyEvent> {
1142 @Override
1143 public void handle(KeyEvent event) {
1144 JLightweightFrame frame = lwFrame;
1145 if (frame == null) {
1146 return;
1147 }
1148 if (event.getCharacter().isEmpty()) {
1149 // TODO: should we post an "empty" character?
1150 return;
1151 }
1152 // Don't let Arrows, Tab, Shift+Tab traverse focus out.
1153 if (event.getCode() == KeyCode.LEFT ||
1154 event.getCode() == KeyCode.RIGHT ||
1155 event.getCode() == KeyCode.UP ||
1156 event.getCode() == KeyCode.DOWN ||
1157 event.getCode() == KeyCode.TAB)
1158 {
1159 event.consume();
1160 }
1161
1162 int swingID = SwingEvents.fxKeyEventTypeToKeyID(event);
1163 if (swingID < 0) {
1164 return;
1165 }
1166 int swingModifiers = SwingEvents.fxKeyModsToKeyMods(event);
1167 int swingKeyCode = event.getCode().getCode();
1168 char swingChar = event.getCharacter().charAt(0);
1169
1170 // A workaround. Some swing L&F's process mnemonics on KEY_PRESSED,
1171 // for which swing provides a keychar. Extracting it from the text.
1172 if (event.getEventType() == javafx.scene.input.KeyEvent.KEY_PRESSED) {
1173 String text = event.getText();
1174 if (text.length() == 1) {
1175 swingChar = text.charAt(0);
1176 }
1177 }
1178 long swingWhen = System.currentTimeMillis();
1179 java.awt.event.KeyEvent keyEvent = new java.awt.event.KeyEvent(
1180 frame, swingID, swingWhen, swingModifiers,
1181 swingKeyCode, swingChar);
1182 AccessController.doPrivileged(new PostEventAction(keyEvent));
1183 }
1184 }
1185 }
|
36 import javafx.scene.input.KeyCode;
37 import javafx.scene.input.KeyEvent;
38 import javafx.scene.input.MouseButton;
39 import javafx.scene.input.MouseEvent;
40 import javafx.scene.input.ScrollEvent;
41 import javafx.stage.Window;
42 import javax.swing.JComponent;
43 import javax.swing.Timer;
44 import java.awt.AWTEvent;
45 import java.awt.Component;
46 import java.awt.Cursor;
47 import java.awt.EventQueue;
48 import java.awt.Toolkit;
49 import java.lang.ref.WeakReference;
50 import java.awt.dnd.DragGestureEvent;
51 import java.awt.dnd.DragGestureListener;
52 import java.awt.dnd.DragGestureRecognizer;
53 import java.awt.dnd.DragSource;
54 import java.awt.dnd.DropTarget;
55 import java.awt.dnd.InvalidDnDOperationException;
56 import java.awt.event.InputEvent;
57 import java.awt.event.MouseWheelEvent;
58 import java.awt.event.WindowEvent;
59 import java.awt.event.WindowFocusListener;
60 import java.lang.reflect.Method;
61 import java.nio.IntBuffer;
62 import java.security.AccessController;
63 import java.security.PrivilegedAction;
64 import java.util.ArrayList;
65 import java.util.HashSet;
66 import java.util.List;
67 import java.util.Set;
68 import java.util.concurrent.locks.ReentrantLock;
69 import com.sun.javafx.embed.swing.Disposer;
70 import com.sun.javafx.embed.swing.DisposerRecord;
71 import com.sun.javafx.geom.BaseBounds;
72 import com.sun.javafx.geom.transform.BaseTransform;
73 import com.sun.javafx.scene.DirtyBits;
74 import com.sun.javafx.sg.prism.NGExternalNode;
75 import com.sun.javafx.sg.prism.NGNode;
76 import com.sun.javafx.stage.FocusUngrabEvent;
77 import com.sun.javafx.stage.WindowHelper;
78 import com.sun.javafx.tk.TKStage;
79 import com.sun.javafx.PlatformUtil;
80 import com.sun.javafx.embed.swing.SwingNodeHelper;
81 import com.sun.javafx.scene.NodeHelper;
82
83 import jdk.swing.interop.LightweightFrameWrapper;
84 import jdk.swing.interop.LightweightContentWrapper;
85 import jdk.swing.interop.DragSourceContextWrapper;
86
87 import static javafx.stage.WindowEvent.WINDOW_HIDDEN;
88
89 /**
90 * This class is used to embed a Swing content into a JavaFX application.
91 * The content to be displayed is specified with the {@link #setContent} method
92 * that accepts an instance of Swing {@code JComponent}. The hierarchy of components
93 * contained in the {@code JComponent} instance should not contain any heavyweight
94 * components, otherwise {@code SwingNode} may fail to paint it. The content gets
95 * repainted automatically. All the input and focus events are forwarded to the
96 * {@code JComponent} instance transparently to the developer.
97 * <p>
98 * Here is a typical pattern which demonstrates how {@code SwingNode} can be used:
99 * <pre>
100 * public class SwingFx extends Application {
101 *
102 * @Override
103 * public void start(Stage stage) {
104 * final SwingNode swingNode = new SwingNode();
105 * createAndSetSwingContent(swingNode);
159 }
160
161 @Override
162 public boolean doComputeContains(Node node, double localX, double localY) {
163 return ((SwingNode) node).doComputeContains(localX, localY);
164 }
165 });
166 }
167
168 private double fxWidth;
169 private double fxHeight;
170
171 private int swingPrefWidth;
172 private int swingPrefHeight;
173 private int swingMaxWidth;
174 private int swingMaxHeight;
175 private int swingMinWidth;
176 private int swingMinHeight;
177
178 private volatile JComponent content;
179 private volatile LightweightFrameWrapper lwFrame;
180 final LightweightFrameWrapper getLightweightFrame() { return lwFrame; }
181
182 private NGExternalNode peer;
183
184 private final ReentrantLock paintLock = new ReentrantLock();
185
186 private boolean skipBackwardUnrgabNotification;
187 private boolean grabbed; // lwframe initiated grab
188 private Timer deactivate; // lwFrame deactivate delay for Linux
189
190 {
191 // To initialize the class helper at the begining each constructor of this class
192 SwingNodeHelper.initHelper(this);
193 }
194
195 /**
196 * Constructs a new instance of {@code SwingNode}.
197 */
198 public SwingNode() {
199 setFocusTraversable(true);
200 setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler());
314 }
315
316 public boolean isIntegerApi() {
317 return isIntegerAPI;
318 }
319
320 public Object invoke(T object, Object... args) {
321 if (method != null) {
322 try {
323 return method.invoke(object, args);
324 } catch (Throwable ex) {
325 throw new RuntimeException("Error when calling " + object.getClass().getName() + "." + method.getName() + "().", ex);
326 }
327 } else {
328 return null;
329 }
330 }
331 }
332
333 /**
334 * Calls LightweightFrameWrapper.notifyDisplayChanged.
335 * Must be called on EDT only.
336 */
337 private static OptionalMethod<LightweightFrameWrapper> jlfNotifyDisplayChanged;
338 private static OptionalMethod<LightweightFrameWrapper> jlfOverrideNativeWindowHandle;
339
340 static {
341 jlfNotifyDisplayChanged = new OptionalMethod<>(LightweightFrameWrapper.class,
342 "notifyDisplayChanged", Double.TYPE, Double.TYPE);
343 if (!jlfNotifyDisplayChanged.isSupported()) {
344 jlfNotifyDisplayChanged = new OptionalMethod<>(
345 LightweightFrameWrapper.class,"notifyDisplayChanged", Integer.TYPE);
346 }
347
348 jlfOverrideNativeWindowHandle = new OptionalMethod<>(LightweightFrameWrapper.class,
349 "overrideNativeWindowHandle", Long.TYPE, Runnable.class);
350
351 }
352
353 /*
354 * Called on EDT
355 */
356 private void setContentImpl(JComponent content) {
357 if (lwFrame != null) {
358 lwFrame.dispose();
359 lwFrame = null;
360 }
361 if (content != null) {
362 lwFrame = new LightweightFrameWrapper();
363
364 SwingNodeWindowFocusListener snfListener =
365 new SwingNodeWindowFocusListener(this);
366 lwFrame.addWindowFocusListener(snfListener);
367
368 if (getScene() != null) {
369 Window window = getScene().getWindow();
370 if (window != null) {
371 if (jlfNotifyDisplayChanged.isIntegerApi()) {
372 jlfNotifyDisplayChanged.invoke(lwFrame,
373 (int) Math.round(window.getRenderScaleX()));
374 } else {
375 jlfNotifyDisplayChanged.invoke(lwFrame,
376 window.getRenderScaleX(),
377 window.getRenderScaleY());
378 }
379 }
380 }
381 lwFrame.setContent(new SwingNodeContent(content, this));
382 lwFrame.setVisible(true);
542 * @return the minimum height that the node should be resized to during layout
543 */
544 @Override public double minHeight(double width) {
545 return swingMinHeight;
546 }
547
548 /*
549 * Note: This method MUST only be called via its accessor method.
550 */
551 private boolean doComputeContains(double localX, double localY) {
552 return true;
553 }
554
555 private final InvalidationListener locationListener = observable -> {
556 locateLwFrame();
557 };
558
559 private final EventHandler<FocusUngrabEvent> ungrabHandler = event -> {
560 if (!skipBackwardUnrgabNotification) {
561 if (lwFrame != null) {
562 AccessController.doPrivileged(new PostEventAction(
563 lwFrame.createUngrabEvent(lwFrame)));
564 }
565 }
566 };
567
568 private final ChangeListener<Boolean> windowVisibleListener = (observable, oldValue, newValue) -> {
569 if (!newValue) {
570 disposeLwFrame();
571 } else {
572 setContent(content);
573 }
574 };
575
576 private final ChangeListener<Window> sceneWindowListener = (observable, oldValue, newValue) -> {
577 if (oldValue != null) {
578 removeWindowListeners(oldValue);
579 }
580
581 notifyNativeHandle(newValue);
582
583 if (newValue != null) {
657 setLwFrameVisible(newValue);
658 });
659
660 return peer;
661 }
662
663 /*
664 * Note: This method MUST only be called via its accessor method.
665 */
666 private void doUpdatePeer() {
667 if (NodeHelper.isDirty(this, DirtyBits.NODE_VISIBLE)
668 || NodeHelper.isDirty(this, DirtyBits.NODE_BOUNDS)) {
669 locateLwFrame(); // initialize location
670 }
671 if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
672 peer.markContentDirty();
673 }
674 }
675
676 /**
677 * Calls LightweightFrameWrapper.setHostBounds.
678 * Must be called on EDT only.
679 */
680 private static final OptionalMethod<LightweightFrameWrapper> jlfSetHostBounds =
681 new OptionalMethod<>(LightweightFrameWrapper.class, "setHostBounds",
682 Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE);
683
684 private void locateLwFrame() {
685 if (getScene() == null
686 || lwFrame == null
687 || getScene().getWindow() == null
688 || !getScene().getWindow().isShowing()) {
689 // Not initialized yet. Skip the update to set the real values later
690 return;
691 }
692 Window w = getScene().getWindow();
693 double renderScaleX = w.getRenderScaleX();
694 double renderScaleY = w.getRenderScaleY();
695 final Point2D loc = localToScene(0, 0);
696 final int windowX = (int) (w.getX());
697 final int windowY = (int) (w.getY());
698 final int windowW = (int) (w.getWidth());
699 final int windowH = (int) (w.getHeight());
700 final int frameX = (int) Math.round(w.getX() + getScene().getX() + loc.getX());
701 final int frameY = (int) Math.round(w.getY() + getScene().getY() + loc.getY());
781 if (jlfNotifyDisplayChanged.isIntegerApi()) {
782 jlfNotifyDisplayChanged.invoke(lwFrame,
783 (int)Math.round(scaleX));
784 } else {
785 jlfNotifyDisplayChanged.invoke(lwFrame, scaleX, scaleY);
786 }
787 }
788 });
789 }
790
791 /*
792 * Note: This method MUST only be called via its accessor method.
793 */
794 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
795 bounds.deriveWithNewBounds(0, 0, 0, (float)fxWidth, (float)fxHeight, 0);
796 tx.transform(bounds, bounds);
797 return bounds;
798 }
799
800 private static class SwingNodeDisposer implements DisposerRecord {
801 LightweightFrameWrapper lwFrame;
802
803 SwingNodeDisposer(LightweightFrameWrapper ref) {
804 this.lwFrame = ref;
805 }
806 public void dispose() {
807 if (lwFrame != null) {
808 lwFrame.dispose();
809 lwFrame = null;
810 }
811 }
812 }
813
814 private static class SwingNodeWindowFocusListener implements WindowFocusListener {
815 private WeakReference<SwingNode> swingNodeRef;
816
817 SwingNodeWindowFocusListener(SwingNode swingNode) {
818 this.swingNodeRef = new WeakReference<SwingNode>(swingNode);
819 }
820
821 @Override
822 public void windowGainedFocus(WindowEvent e) {
823 SwingFXUtils.runOnFxThread(() -> {
824 SwingNode swingNode = swingNodeRef.get();
825 if (swingNode != null) {
826 swingNode.requestFocus();
827 }
828 });
829 }
830
831 @Override
832 public void windowLostFocus(WindowEvent e) {
833 SwingFXUtils.runOnFxThread(() -> {
834 SwingNode swingNode = swingNodeRef.get();
835 if (swingNode != null) {
836 swingNode.ungrabFocus(true);
837 }
838 });
839 }
840 }
841
842 private static class SwingNodeContent extends LightweightContentWrapper {
843 private JComponent comp;
844 private volatile FXDnD dnd;
845 private WeakReference<SwingNode> swingNodeRef;
846
847 SwingNodeContent(JComponent comp, SwingNode swingNode) {
848 this.comp = comp;
849 this.swingNodeRef = new WeakReference<SwingNode>(swingNode);
850 }
851 @Override
852 public JComponent getComponent() {
853 return comp;
854 }
855 @Override
856 public void paintLock() {
857 SwingNode swingNode = swingNodeRef.get();
858 if (swingNode != null) {
859 swingNode.paintLock.lock();
860 }
861 }
862 @Override
863 public void paintUnlock() {
864 SwingNode swingNode = swingNodeRef.get();
865 if (swingNode != null) {
866 swingNode.paintLock.unlock();
867 }
868 }
869
870 @Override
871 public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride) {
872 imageBufferReset(data, x, y, width, height, linestride, 1);
873 }
874 //@Override
875 public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride, int scale) {
876 SwingNode swingNode = swingNodeRef.get();
877 if (swingNode != null) {
878 swingNode.setImageBuffer(data, x, y, width, height, linestride, scale, scale);
879 }
880 }
881 @Override
882 public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride, double scaleX, double scaleY) {
883 SwingNode swingNode = swingNodeRef.get();
884 if (swingNode != null) {
885 swingNode.setImageBuffer(data, x, y, width, height, linestride, scaleX, scaleY);
886 }
887 }
888 @Override
889 public void imageReshaped(int x, int y, int width, int height) {
890 SwingNode swingNode = swingNodeRef.get();
891 if (swingNode != null) {
892 swingNode.setImageBounds(x, y, width, height);
893 }
894 }
895 @Override
896 public void imageUpdated(int dirtyX, int dirtyY, int dirtyWidth, int dirtyHeight) {
897 SwingNode swingNode = swingNodeRef.get();
898 if (swingNode != null) {
899 swingNode.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
900 }
901 }
966 SwingFXUtils.runOnFxThread(() -> {
967 SwingNode swingNode = swingNodeRef.get();
968 if (swingNode != null) {
969 swingNode.setCursor(SwingCursors.embedCursorToCursor(cursor));
970 }
971 });
972 }
973
974 private void initDnD() {
975 // This is a part of AWT API, so the method may be invoked on any thread
976 synchronized (SwingNodeContent.this) {
977 if (this.dnd == null) {
978 SwingNode swingNode = swingNodeRef.get();
979 if (swingNode != null) {
980 this.dnd = new FXDnD(swingNode);
981 }
982 }
983 }
984 }
985
986 @Override
987 public synchronized <T extends DragGestureRecognizer> T createDragGestureRecognizer(
988 Class<T> abstractRecognizerClass,
989 DragSource ds, Component c, int srcActions,
990 DragGestureListener dgl)
991 {
992 initDnD();
993 return dnd.createDragGestureRecognizer(abstractRecognizerClass, ds, c, srcActions, dgl);
994 }
995
996 @Override
997 public DragSourceContextWrapper createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException
998 {
999 initDnD();
1000 return dnd.createDragSourceContextPeer(dge);
1001 }
1002
1003 @Override
1004 public void addDropTarget(DropTarget dt) {
1005 initDnD();
1006 dnd.addDropTarget(dt);
1007 }
1008
1009 @Override
1010 public void removeDropTarget(DropTarget dt) {
1011 initDnD();
1012 dnd.removeDropTarget(dt);
1013 }
1014 }
1015
1016 private void ungrabFocus(boolean postUngrabEvent) {
1017 // On X11 grab is limited to a single XDisplay connection,
1018 // so we can't delegate it to another GUI toolkit.
1019 if (PlatformUtil.isLinux()) return;
1020
1021 if (grabbed &&
1022 getScene() != null &&
1023 getScene().getWindow() != null &&
1024 WindowHelper.getPeer(getScene().getWindow()) != null)
1025 {
1026 skipBackwardUnrgabNotification = !postUngrabEvent;
1027 WindowHelper.getPeer(getScene().getWindow()).ungrabFocus();
1028 skipBackwardUnrgabNotification = false;
1029 grabbed = false;
1031 }
1032
1033 private class PostEventAction implements PrivilegedAction<Void> {
1034 private AWTEvent event;
1035 PostEventAction(AWTEvent event) {
1036 this.event = event;
1037 }
1038 @Override
1039 public Void run() {
1040 EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue();
1041 eq.postEvent(event);
1042 return null;
1043 }
1044 }
1045
1046 private class SwingMouseEventHandler implements EventHandler<MouseEvent> {
1047 private final Set<MouseButton> mouseClickedAllowed = new HashSet<>();
1048
1049 @Override
1050 public void handle(MouseEvent event) {
1051 LightweightFrameWrapper frame = lwFrame;
1052 if (frame == null) {
1053 return;
1054 }
1055 int swingID = SwingEvents.fxMouseEventTypeToMouseID(event);
1056 if (swingID < 0) {
1057 return;
1058 }
1059
1060 // Prevent ancestors of the SwingNode from stealing the focus
1061 event.consume();
1062
1063 final EventType<?> type = event.getEventType();
1064 if (type == MouseEvent.MOUSE_PRESSED) {
1065 mouseClickedAllowed.add(event.getButton());
1066 } else if (type == MouseEvent.MOUSE_RELEASED) {
1067 // RELEASED comes before CLICKED, so we don't remove the button from the set
1068 //mouseClickedAllowed.remove(event.getButton());
1069 } else if (type == MouseEvent.MOUSE_DRAGGED) {
1070 // This is what AWT/Swing do
1071 mouseClickedAllowed.clear();
1072 } else if (type == MouseEvent.MOUSE_CLICKED) {
1073 if (event.getClickCount() == 1 && !mouseClickedAllowed.contains(event.getButton())) {
1074 // RT-34610: In FX, CLICKED events are generated even after dragging the mouse pointer
1075 // Note that this is only relevant for single clicks. Double clicks use a smudge factor.
1076 return;
1077 }
1078 mouseClickedAllowed.remove(event.getButton());
1079 }
1080 int swingModifiers = SwingEvents.fxMouseModsToMouseMods(event);
1081 boolean swingPopupTrigger = event.isPopupTrigger();
1082 int swingButton = SwingEvents.fxMouseButtonToMouseButton(event);
1083 long swingWhen = System.currentTimeMillis();
1084 int relX = (int) Math.round(event.getX());
1085 int relY = (int) Math.round(event.getY());
1086 int absX = (int) Math.round(event.getScreenX());
1087 int absY = (int) Math.round(event.getScreenY());
1088 java.awt.event.MouseEvent mouseEvent =
1089 frame.createMouseEvent(
1090 frame, swingID, swingWhen, swingModifiers,
1091 relX, relY, absX, absY,
1092 event.getClickCount(), swingPopupTrigger, swingButton);
1093 AccessController.doPrivileged(new PostEventAction(mouseEvent));
1094 }
1095 }
1096
1097 private class SwingScrollEventHandler implements EventHandler<ScrollEvent> {
1098 @Override
1099 public void handle(ScrollEvent event) {
1100 LightweightFrameWrapper frame = lwFrame;
1101 if (frame == null) {
1102 return;
1103 }
1104
1105 int swingModifiers = SwingEvents.fxScrollModsToMouseWheelMods(event);
1106 final boolean isShift = (swingModifiers & InputEvent.SHIFT_DOWN_MASK) != 0;
1107
1108 // Vertical scroll.
1109 if (!isShift && event.getDeltaY() != 0.0) {
1110 sendMouseWheelEvent(frame, event.getX(), event.getY(),
1111 swingModifiers, event.getDeltaY() / event.getMultiplierY());
1112 }
1113 // Horizontal scroll or shirt+vertical scroll.
1114 final double delta = isShift && event.getDeltaY() != 0.0
1115 ? event.getDeltaY() / event.getMultiplierY()
1116 : event.getDeltaX() / event.getMultiplierX();
1117 if (delta != 0.0) {
1118 swingModifiers |= InputEvent.SHIFT_DOWN_MASK;
1119 sendMouseWheelEvent(frame, event.getX(), event.getY(),
1120 swingModifiers, delta);
1121 }
1122 }
1123
1124 private void sendMouseWheelEvent(LightweightFrameWrapper source, double fxX, double fxY, int swingModifiers, double delta) {
1125 int wheelRotation = (int) delta;
1126 int signum = (int) Math.signum(delta);
1127 if (signum * delta < 1) {
1128 wheelRotation = signum;
1129 }
1130 int x = (int) Math.round(fxX);
1131 int y = (int) Math.round(fxY);
1132 MouseWheelEvent mouseWheelEvent =
1133 lwFrame.createMouseWheelEvent(source, swingModifiers, x, y, -wheelRotation);
1134 AccessController.doPrivileged(new PostEventAction(mouseWheelEvent));
1135 }
1136 }
1137
1138 private class SwingKeyEventHandler implements EventHandler<KeyEvent> {
1139 @Override
1140 public void handle(KeyEvent event) {
1141 LightweightFrameWrapper frame = lwFrame;
1142 if (frame == null) {
1143 return;
1144 }
1145 if (event.getCharacter().isEmpty()) {
1146 // TODO: should we post an "empty" character?
1147 return;
1148 }
1149 // Don't let Arrows, Tab, Shift+Tab traverse focus out.
1150 if (event.getCode() == KeyCode.LEFT ||
1151 event.getCode() == KeyCode.RIGHT ||
1152 event.getCode() == KeyCode.UP ||
1153 event.getCode() == KeyCode.DOWN ||
1154 event.getCode() == KeyCode.TAB)
1155 {
1156 event.consume();
1157 }
1158
1159 int swingID = SwingEvents.fxKeyEventTypeToKeyID(event);
1160 if (swingID < 0) {
1161 return;
1162 }
1163 int swingModifiers = SwingEvents.fxKeyModsToKeyMods(event);
1164 int swingKeyCode = event.getCode().getCode();
1165 char swingChar = event.getCharacter().charAt(0);
1166
1167 // A workaround. Some swing L&F's process mnemonics on KEY_PRESSED,
1168 // for which swing provides a keychar. Extracting it from the text.
1169 if (event.getEventType() == javafx.scene.input.KeyEvent.KEY_PRESSED) {
1170 String text = event.getText();
1171 if (text.length() == 1) {
1172 swingChar = text.charAt(0);
1173 }
1174 }
1175 long swingWhen = System.currentTimeMillis();
1176 java.awt.event.KeyEvent keyEvent = frame.createKeyEvent(frame,
1177 swingID, swingWhen, swingModifiers, swingKeyCode,
1178 swingChar);
1179 AccessController.doPrivileged(new PostEventAction(keyEvent));
1180 }
1181 }
1182 }
|