Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.embed.swing;
027
028import com.sun.javafx.geom.BaseBounds;
029import com.sun.javafx.geom.transform.BaseTransform;
030import com.sun.javafx.jmx.MXNodeAlgorithm;
031import com.sun.javafx.jmx.MXNodeAlgorithmContext;
032import com.sun.javafx.scene.DirtyBits;
033import com.sun.javafx.scene.traversal.Direction;
034import com.sun.javafx.sg.PGNode;
035import com.sun.javafx.sg.PGExternalNode;
036import com.sun.javafx.stage.FocusUngrabEvent;
037
038import javafx.application.Platform;
039import javafx.beans.InvalidationListener;
040import javafx.beans.Observable;
041import javafx.beans.value.ObservableValue;
042import javafx.event.EventHandler;
043import javafx.scene.Node;
044import javafx.scene.input.KeyEvent;
045import javafx.scene.input.MouseButton;
046import javafx.scene.input.MouseEvent;
047import javafx.beans.value.ChangeListener;
048import javafx.geometry.Point2D;
049import javafx.scene.input.KeyCode;
050
051import javax.swing.JComponent;
052import javax.swing.SwingUtilities;
053import java.awt.AWTEvent;
054import java.awt.EventQueue;
055import java.awt.Toolkit;
056import java.awt.event.WindowEvent;
057import java.awt.event.WindowFocusListener;
058
059import java.nio.IntBuffer;
060import java.security.AccessController;
061import java.security.PrivilegedAction;
062import java.util.ArrayList;
063import java.util.List;
064import java.util.concurrent.locks.ReentrantLock;
065
066import javafx.scene.Scene;
067import javafx.stage.Window;
068import sun.awt.UngrabEvent;
069
070import sun.swing.LightweightContent;
071import sun.swing.JLightweightFrame;
072
073/**
074 * This class is used to embed a Swing content into a JavaFX application.
075 * The content to be displayed is specified with the {@link #setContent} method
076 * that accepts an instance of Swing {@code JComponent}. The hierarchy of components
077 * contained in the {@code JComponent} instance should not contain any heavyweight
078 * components, otherwise {@code SwingNode} may fail to paint it. The content gets
079 * repainted automatically. All the input and focus events are forwarded to the
080 * {@code JComponent} instance transparently to the developer.
081 * <p>
082 * Here is a typical pattern which demonstrates how {@code SwingNode} can be used:
083 * <pre>
084 *     public class SwingFx extends Application {
085 *
086 *         private SwingNode swingNode;
087 *
088 *         &#064;Override
089 *         public void start(Stage stage) {
090 *             swingNode = new SwingNode();
091 *
092 *             createAndSetSwingContent();
093 *
094 *             StackPane pane = new StackPane();
095 *             pane.getChildren().add(swingNode);
096 *
097 *             stage.setScene(new Scene(pane, 100, 50));
098 *             stage.show();
099 *         }
100 *
101 *         private void createAndSetSwingContent() {
102 *             SwingUtilities.invokeLater(new Runnable() {
103 *                 &#064;Override
104 *                 public void run() {
105 *                     swingNode.setContent(new JButton("Click me!"));
106 *                 }
107 *             });
108 *         }
109 *     }
110 * </pre>
111 */
112public class SwingNode extends Node {
113
114    private double width;
115    private double height;
116
117    private volatile JComponent content;
118    private volatile JLightweightFrame lwFrame;
119
120    private volatile PGExternalNode peer;
121
122    private final ReentrantLock paintLock = new ReentrantLock();
123
124    private boolean skipBackwardUnrgabNotification;
125    private boolean grabbed; // lwframe initiated grab
126    
127    /**
128     * Constructs a new instance of {@code SwingNode}.
129     */
130    public SwingNode() {
131        setFocusTraversable(true);
132        setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler());
133        setEventHandler(KeyEvent.ANY, new SwingKeyEventHandler());
134
135        focusedProperty().addListener(new ChangeListener<Boolean>() {
136            @Override
137            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, final Boolean newValue) {
138                 activateLwFrame(newValue);
139            }
140        });
141    }
142
143    /**
144     * Attaches a {@code JComponent} instance to display in this {@code SwingNode}.
145     * <p>
146     * The method can be called either on the JavaFX Application thread or the Swing thread.
147     * Note however, that access to a Swing component must occur from the Swing thread according
148     * to the Swing threading restrictions.
149     *
150     * @param content a Swing component to display in this {@code SwingNode}
151     *
152     * @see java.awt.EventQueue#isDispatchThread()
153     * @see javafx.application.Platform#isFxApplicationThread()
154     */
155    public void setContent(final JComponent content) {
156        this.content = content;
157
158        invokeOnEDT(new Runnable() {
159            @Override
160            public void run() {
161                setContentImpl(content);
162            }
163        });
164    }
165
166   /**
167     * Returns the {@code JComponent} instance attached to this {@code SwingNode}.
168     * <p>
169     * The method can be called either on the JavaFX Application thread or the Swing thread.
170     * Note however, that access to a Swing component must occur from the Swing thread according
171     * to the Swing threading restrictions.
172     *
173     * @see java.awt.EventQueue#isDispatchThread()
174     * @see javafx.application.Platform#isFxApplicationThread()
175     *
176     * @return the Swing component attached to this {@code SwingNode}
177     */
178    public JComponent getContent() {
179        return content;
180    }
181
182    /*
183     * Called on Swing thread
184     */
185    private void setContentImpl(JComponent content) {
186        if (lwFrame != null) {
187            lwFrame.dispose();
188            lwFrame = null;
189        }
190        if (content != null) {
191            lwFrame = new JLightweightFrame();
192
193            lwFrame.addWindowFocusListener(new WindowFocusListener() {
194                @Override
195                public void windowGainedFocus(WindowEvent e) {
196                }
197                @Override
198                public void windowLostFocus(WindowEvent e) {
199                    Platform.runLater(new Runnable() {
200                        @Override
201                        public void run() {
202                            ungrabFocus(true);
203                        }
204                    });
205                }
206            });
207
208            lwFrame.setContent(new SwingNodeContent(content));
209            lwFrame.setVisible(true);
210
211            locateLwFrame(); // initialize location
212
213            if (focusedProperty().get()) {
214                activateLwFrame(true);
215            }
216        }
217    }
218
219    private List<Runnable> peerRequests = new ArrayList<>();
220
221    /*
222     * Called on Swing thread
223     */
224    void setImageBuffer(final int[] data,
225                        final int x, final int y,
226                        final int w, final int h,
227                        final int linestride)
228    {
229        Runnable r = new Runnable() {
230            @Override
231            public void run() {
232                peer.setImageBuffer(IntBuffer.wrap(data), x, y, w, h, linestride);
233            }
234        };
235        if (peer != null) {
236            Platform.runLater(r);
237        } else {
238            peerRequests.clear();
239            peerRequests.add(r);
240        }
241    }
242
243    /*
244     * Called on Swing thread
245     */
246    void setImageBounds(final int x, final int y, final int w, final int h) {
247        Runnable r = new Runnable() {
248            @Override
249            public void run() {
250                peer.setImageBounds(x, y, w, h);
251            }
252        };
253        if (peer != null) {
254            Platform.runLater(r);
255        } else {
256            peerRequests.add(r);
257        }
258    }
259
260    /*
261     * Called on Swing thread
262     */
263    void repaintDirtyRegion(final int dirtyX, final int dirtyY, final int dirtyWidth, final int dirtyHeight) {
264        Runnable r = new Runnable() {
265            @Override
266            public void run() {
267                peer.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
268                impl_markDirty(DirtyBits.NODE_CONTENTS);
269            }
270        };
271        if (peer != null) {
272            Platform.runLater(r);
273        } else {
274            peerRequests.add(r);
275        }
276    }
277
278    @Override public boolean isResizable() {
279        return true;
280    }
281
282    @Override public void resize(final double width, final double height) {
283        this.width = width;
284        this.height = height;
285        super.resize(width, height);
286        impl_geomChanged();
287        impl_markDirty(DirtyBits.NODE_GEOMETRY);
288        SwingUtilities.invokeLater(new Runnable() {
289            @Override
290            public void run() {
291                if (lwFrame != null) {
292                    lwFrame.setSize((int)width, (int)height);
293                }
294            }
295        });
296    }
297
298    @Override
299    public double maxWidth(double height) {
300        return Double.MAX_VALUE;
301    }
302
303    @Override
304    public double maxHeight(double width) {
305        return Double.MAX_VALUE;
306    }
307
308    @Override
309    public double prefWidth(double height) {
310        return -1;
311    }
312
313    @Override
314    public double prefHeight(double width) {
315        return -1;
316    }
317
318    @Override
319    public double minWidth(double height) {
320        return 0;
321    }
322
323    @Override
324    public double minHeight(double width) {
325        return 0;
326    }
327
328    @Override
329    protected boolean impl_computeContains(double localX, double localY) {
330        return true;
331    }
332
333    private InvalidationListener locationListener = new InvalidationListener() {
334        @Override
335        public void invalidated(Observable observable) {
336            locateLwFrame();
337        }
338    };
339
340    private EventHandler<FocusUngrabEvent> ungrabHandler = new EventHandler<FocusUngrabEvent>() {
341        @Override
342        public void handle(FocusUngrabEvent event) {
343            if (!skipBackwardUnrgabNotification) {
344                AccessController.doPrivileged(new PostEventAction(new UngrabEvent(lwFrame)));
345            }
346        }
347    };
348
349    private ChangeListener<Boolean> windowVisibleListener = new ChangeListener<Boolean>() {
350        @Override
351        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
352            if (!newValue) {
353                disposeLwFrame();
354
355            } else {
356                setContent(content);
357            }
358        }
359    };
360
361    private void removeListeners(Scene scene) {
362        Window window = scene.getWindow();
363        if (window != null) {
364            window.xProperty().removeListener(locationListener);
365            window.yProperty().removeListener(locationListener);
366            window.removeEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler);
367            window.showingProperty().removeListener(windowVisibleListener);
368        }
369    }
370
371    private void addListeners(Scene scene) {
372        Window window = scene.getWindow();
373        if (window != null) {
374            window.xProperty().addListener(locationListener);
375            window.yProperty().addListener(locationListener);
376            window.addEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler);
377            window.showingProperty().addListener(windowVisibleListener);
378        }
379    }
380
381    @Override
382    protected PGNode impl_createPGNode() {
383        peer = com.sun.javafx.tk.Toolkit.getToolkit().createPGExternalNode();
384        peer.setLock(paintLock);
385        for (Runnable request : peerRequests) {
386            request.run();
387        }
388        peerRequests = null;
389
390        if (content != null) {
391            setContent(content); // in case the Node is re-added to Scene
392        }
393        addListeners(getScene());
394
395        sceneProperty().addListener(new ChangeListener<Scene>() {
396            @Override
397            public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
398                // Removed from scene, or added to another scene.
399                // The lwFrame will be recreated from impl_createPGNode().
400                removeListeners(oldValue);
401                disposeLwFrame();
402            }
403        });
404
405        impl_treeVisibleProperty().addListener(new ChangeListener<Boolean>() {
406            @Override
407            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
408                setLwFrameVisible(newValue);
409            }
410        });
411
412        return peer;
413    }
414
415    @Override
416    public void impl_updatePG() {
417        super.impl_updatePG();
418
419        if (impl_isDirty(DirtyBits.NODE_VISIBLE)) {
420            locateLwFrame(); // initialize location
421        }
422        if (impl_isDirty(DirtyBits.NODE_CONTENTS)) {
423            peer.markContentDirty();
424        }
425    }
426
427    private void locateLwFrame() {
428        if (getScene() == null || lwFrame == null) {
429            return;
430        }
431        final Point2D loc = localToScene(0, 0);
432        final int windowX = (int)getScene().getWindow().getX();
433        final int windowY = (int)getScene().getWindow().getY();
434        final int sceneX = (int)getScene().getX();
435        final int sceneY = (int)getScene().getY();
436
437        invokeOnEDT(new Runnable() {
438            @Override
439            public void run() {
440                if (lwFrame != null) {
441                    lwFrame.setLocation(windowX + sceneX + (int)loc.getX(),
442                                        windowY + sceneY + (int)loc.getY());
443                }
444            }
445        });
446    }
447
448    private void activateLwFrame(final boolean activate) {
449        if (lwFrame == null) {
450            return;
451        }
452        invokeOnEDT(new Runnable() {
453            @Override
454            public void run() {
455                if (lwFrame != null) {
456                    lwFrame.emulateActivation(activate);
457                }
458            }
459        });
460    }
461
462    private void disposeLwFrame() {
463        if (lwFrame == null) {
464            return;
465        }
466        invokeOnEDT(new Runnable() {
467            @Override
468            public void run() {
469                if (lwFrame != null) {
470                    lwFrame.dispose();
471                    lwFrame = null;
472                }
473            }
474        });
475    }
476
477    private void setLwFrameVisible(final boolean visible) {
478        if (lwFrame == null) {
479            return;
480        }
481        invokeOnEDT(new Runnable() {
482            @Override
483            public void run() {
484                if (lwFrame != null) {
485                    lwFrame.setVisible(visible);
486                }
487            }
488        });
489    }
490
491    @Override
492    public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
493        bounds.deriveWithNewBounds(0, 0, 0, (float)width, (float)height, 0);
494        tx.transform(bounds, bounds);
495        return bounds;
496    }
497
498    @Override
499    public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
500        return alg.processLeafNode(this, ctx);
501    }
502
503    private class SwingNodeContent implements LightweightContent {
504        private JComponent comp;
505        public SwingNodeContent(JComponent comp) {
506            this.comp = comp;
507        }
508        @Override
509        public JComponent getComponent() {
510            return comp;
511        }
512        @Override
513        public void paintLock() {
514            paintLock.lock();
515        }
516        @Override
517        public void paintUnlock() {
518            paintLock.unlock();
519        }
520        @Override
521        public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride) {
522            SwingNode.this.setImageBuffer(data, x, y, width, height, linestride);
523        }
524        @Override
525        public void imageReshaped(int x, int y, int width, int height) {
526            SwingNode.this.setImageBounds(x, y, width, height);
527        }
528        @Override
529        public void imageUpdated(int dirtyX, int dirtyY, int dirtyWidth, int dirtyHeight) {
530            SwingNode.this.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
531        }
532        @Override
533        public void focusGrabbed() {
534            Platform.runLater(new Runnable() {
535                @Override
536                public void run() {
537                    if (getScene() != null && getScene().getWindow() != null) {
538                        getScene().getWindow().impl_getPeer().grabFocus();
539                        grabbed = true;
540                    }
541                }
542            });
543        }
544        @Override
545        public void focusUngrabbed() {
546            Platform.runLater(new Runnable() {
547                @Override
548                public void run() {
549                    ungrabFocus(false);
550                }
551            });
552        }
553    }
554
555    private void ungrabFocus(boolean postUngrabEvent) {
556        if (grabbed &&
557            getScene() != null && getScene().getWindow() != null)
558        {
559            skipBackwardUnrgabNotification = !postUngrabEvent;
560            getScene().getWindow().impl_getPeer().ungrabFocus();
561            skipBackwardUnrgabNotification = false;
562            grabbed = false;
563        }
564    }
565
566    private class PostEventAction implements PrivilegedAction<Void> {
567        private AWTEvent event;
568        public PostEventAction(AWTEvent event) {
569            this.event = event;
570        }
571        @Override
572        public Void run() {
573            EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue();
574            eq.postEvent(event);
575            return null;
576        }
577    }
578
579    private class SwingMouseEventHandler implements EventHandler<MouseEvent> {
580        @Override
581        public void handle(MouseEvent event) {
582            if (event.getEventType() == MouseEvent.MOUSE_PRESSED &&
583                !SwingNode.this.isFocused() && SwingNode.this.isFocusTraversable())
584            {
585                SwingNode.this.requestFocus();
586            }
587            int swingID = SwingEvents.fxMouseEventTypeToMouseID(event);
588            if (swingID < 0) {
589                return;
590            }
591            int swingModifiers = SwingEvents.fxMouseModsToMouseMods(event);
592            // TODO: popupTrigger
593            boolean swingPopupTrigger = event.getButton() == MouseButton.SECONDARY;
594            int swingButton = SwingEvents.fxMouseButtonToMouseButton(event);
595            long swingWhen = System.currentTimeMillis();
596            java.awt.event.MouseEvent mouseEvent =
597                    new java.awt.event.MouseEvent(
598                        lwFrame, swingID, swingWhen, swingModifiers,
599                        (int)event.getX(), (int)event.getY(), (int)event.getScreenX(), (int)event.getSceneY(),
600                        event.getClickCount(), swingPopupTrigger, swingButton);
601            AccessController.doPrivileged(new PostEventAction(mouseEvent));
602        }
603    }
604
605    private class SwingKeyEventHandler implements EventHandler<KeyEvent> {
606        @Override
607        public void handle(KeyEvent event) {
608            if (event.getCharacter().isEmpty()) {
609                // TODO: should we post an "empty" character?
610                return;
611            }
612            // Let Ctrl+Tab, Shift+Strl+Tab traverse focus out.
613            if (event.getCode() == KeyCode.TAB && event.isControlDown()) {
614                Direction d = event.isShiftDown() ? Direction.PREVIOUS : Direction.NEXT;
615                getParent().getImpl_traversalEngine().trav(SwingNode.this, d);
616                return;
617            }
618            // Don't let Arrows, Tab, Shift+Tab traverse focus out.
619            if (event.getCode() == KeyCode.LEFT  ||
620                event.getCode() == KeyCode.RIGHT ||
621                event.getCode() == KeyCode.TAB)
622            {
623                event.consume();
624            }
625
626            int swingID = SwingEvents.fxKeyEventTypeToKeyID(event);
627            if (swingID < 0) {
628                return;
629            }
630            int swingModifiers = SwingEvents.fxKeyModsToKeyMods(event);
631            int swingKeyCode = event.getCode().impl_getCode();
632            char swingChar = event.getCharacter().charAt(0);
633            long swingWhen = System.currentTimeMillis();
634            java.awt.event.KeyEvent keyEvent = new java.awt.event.KeyEvent(
635                    lwFrame, swingID, swingWhen, swingModifiers,
636                    swingKeyCode, swingChar);
637            AccessController.doPrivileged(new PostEventAction(keyEvent));
638        }
639    }
640
641    private static void invokeOnEDT(final Runnable r) {
642        if (SwingUtilities.isEventDispatchThread()) {
643            r.run();
644        } else {
645            SwingUtilities.invokeLater(new Runnable() {
646                @Override
647                public void run() {
648                    r.run();
649                }
650            });
651        }
652    }
653}