Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 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.stage;
027
028import com.sun.javafx.Utils;
029import com.sun.javafx.event.DirectEvent;
030import java.util.ArrayList;
031import java.util.List;
032
033import javafx.beans.InvalidationListener;
034import javafx.beans.Observable;
035import javafx.beans.property.BooleanProperty;
036import javafx.beans.property.BooleanPropertyBase;
037import javafx.beans.property.ObjectProperty;
038import javafx.beans.property.SimpleBooleanProperty;
039import javafx.beans.property.SimpleObjectProperty;
040import javafx.beans.value.ChangeListener;
041import javafx.beans.value.ObservableValue;
042import javafx.collections.ObservableList;
043import javafx.event.Event;
044import javafx.event.EventHandler;
045import javafx.geometry.Bounds;
046import javafx.geometry.Rectangle2D;
047import javafx.scene.Group;
048import javafx.scene.Node;
049import javafx.scene.Parent;
050import javafx.scene.Scene;
051
052import com.sun.javafx.event.EventHandlerManager;
053import com.sun.javafx.event.EventRedirector;
054import com.sun.javafx.event.EventUtil;
055import com.sun.javafx.perf.PerformanceTracker;
056import com.sun.javafx.scene.SceneHelper;
057import com.sun.javafx.stage.FocusUngrabEvent;
058import com.sun.javafx.stage.PopupWindowPeerListener;
059import com.sun.javafx.stage.WindowCloseRequestHandler;
060import com.sun.javafx.stage.WindowEventDispatcher;
061import com.sun.javafx.tk.Toolkit;
062import javafx.beans.property.ReadOnlyObjectProperty;
063import javafx.beans.property.ReadOnlyObjectWrapper;
064import javafx.event.EventTarget;
065import javafx.event.EventType;
066import javafx.scene.input.KeyCombination;
067import javafx.scene.input.KeyEvent;
068import javafx.scene.input.MouseEvent;
069
070/**
071 * PopupWindow is the parent for a variety of different types of popup
072 * based windows including {@link Popup} and {@link javafx.scene.control.Tooltip}
073 * and {@link javafx.scene.control.ContextMenu}.
074 * <p>
075 * A PopupWindow is a secondary window which has no window decorations or title bar.
076 * It doesn't show up in the OS as a top-level window. It is typically
077 * used for tool tip like notification, drop down boxes, menus, and so forth.
078 * <p>
079 * The PopupWindow <strong>cannot be shown without an owner</strong>.
080 * PopupWindows require that an owner window exist in order to be shown. However,
081 * it is possible to create a PopupWindow ahead of time and simply set the owner
082 * (or change the owner) before first being made visible. Attempting to change
083 * the owner while the PopupWindow is visible will result in an IllegalStateException.
084 * <p>
085 * The PopupWindow encapsulates much of the behavior and functionality common to popups,
086 * such as the ability to close when the "esc" key is pressed, or the ability to
087 * hide all child popup windows whenever this window is hidden. These abilities can
088 * be enabled or disabled via properties.
089 */
090public abstract class PopupWindow extends Window {
091    /**
092     * A private list of all child popups.
093     */
094    private final List<PopupWindow> children = new ArrayList<PopupWindow>();
095
096    /**
097     * Keeps track of the bounds of the group, and adjust the size of the
098     * popup window accordingly. This way as the popup content changes, the
099     * window will be changed to match.
100     */
101    private final InvalidationListener rootBoundsListener =
102            new InvalidationListener() {
103                @Override
104                public void invalidated(final Observable observable) {
105                    syncWithRootBounds();
106                }
107            };
108
109    /**
110     * RT-28454: When a parent node or parent window we are associated with is not 
111     * visible anymore, possibly because the scene was not valid anymore, we should hide.
112     */
113    private final ChangeListener<Boolean> ownerNodeListener = new ChangeListener<Boolean>() {
114        @Override public void changed(
115                ObservableValue<? extends Boolean> observable, 
116                Boolean oldValue, Boolean newValue) {
117            if (oldValue && !newValue) {
118                hide();
119            }
120        }
121    };
122    
123    public PopupWindow() {
124        final Scene scene = new Scene(new Group());
125        scene.setFill(null);
126        super.setScene(scene);
127
128        scene.getRoot().layoutBoundsProperty().addListener(rootBoundsListener);
129        scene.rootProperty().addListener(
130                new InvalidationListener() {
131                    private Node oldRoot = scene.getRoot();
132
133                    @Override
134                    public void invalidated(final Observable observable) {
135                        final Node newRoot = scene.getRoot();
136                        if (oldRoot != newRoot) {
137                            if (oldRoot != null) {
138                                oldRoot.layoutBoundsProperty()
139                                       .removeListener(rootBoundsListener);
140                            }
141
142                            if (newRoot != null) {
143                                newRoot.layoutBoundsProperty()
144                                       .addListener(rootBoundsListener);
145                            }
146
147                            oldRoot = newRoot;
148                            syncWithRootBounds();
149                        }
150                    }
151                });
152        syncWithRootBounds();
153    }
154
155    /**
156     * Gets the observable, modifiable list of children which are placed in this
157     * PopupWindow.
158     *
159     * @return the PopupWindow content
160     * @treatAsPrivate implementation detail
161     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
162     */
163    @Deprecated
164    protected ObservableList<Node> getContent() {
165        final Parent rootNode = getScene().getRoot();
166        if (!(rootNode instanceof Group)) {
167            throw new IllegalStateException(
168                    "The content of the Popup can't be accessed");
169        }
170
171        return ((Group) rootNode).getChildren();
172    }
173
174    /**
175     * The window which is the parent of this popup. All popups must have an
176     * owner window.
177     */
178    private ReadOnlyObjectWrapper<Window> ownerWindow =
179            new ReadOnlyObjectWrapper<Window>(this, "ownerWindow");
180    public final Window getOwnerWindow() {
181        return ownerWindow.get();
182    }
183    public final ReadOnlyObjectProperty<Window> ownerWindowProperty() {
184        return ownerWindow.getReadOnlyProperty();
185    }
186
187    /**
188     * The node which is the owner of this popup. All popups must have an
189     * owner window but are not required to be associated with an owner node.
190     * If an autohide Popup has an owner node, mouse press inside the owner node
191     * doesn't cause the Popup to hide.
192     */
193    private ReadOnlyObjectWrapper<Node> ownerNode =
194            new ReadOnlyObjectWrapper<Node>(this, "ownerNode");
195    public final Node getOwnerNode() {
196        return ownerNode.get();
197    }
198    public final ReadOnlyObjectProperty<Node> ownerNodeProperty() {
199        return ownerNode.getReadOnlyProperty();
200    }
201
202    /**
203     * Note to subclasses: the scene used by PopupWindow is very specifically
204     * managed by PopupWindow. This method is overridden to throw
205     * UnsupportedOperationException. You cannot specify your own scene.
206     *
207     * @param scene
208     */
209    @Override protected final void setScene(Scene scene) {
210        throw new UnsupportedOperationException();
211    }
212
213    /**
214     * This convenience variable indicates whether, when the popup is shown,
215     * it should automatically correct its position such that it doesn't end
216     * up positioned off the screen.
217     * @defaultValue true
218     */
219    private BooleanProperty autoFix =
220            new BooleanPropertyBase(true) {
221                @Override
222                protected void invalidated() {
223                    handleAutofixActivation(isShowing(), get());
224                }
225
226                @Override
227                public Object getBean() {
228                    return PopupWindow.this;
229                }
230
231                @Override
232                public String getName() {
233                    return "autoFix";
234                }
235            };
236    public final void setAutoFix(boolean value) { autoFix.set(value); }
237    public final boolean isAutoFix() { return autoFix.get(); }
238    public final BooleanProperty autoFixProperty() { return autoFix; }
239
240    /**
241     * Specifies whether Popups should auto hide. If a popup loses focus and
242     * autoHide is true, then the popup will be hidden automatically.
243     * @defaultValue false
244     */
245    private BooleanProperty autoHide =
246            new SimpleBooleanProperty(this, "autoHide");
247    public final void setAutoHide(boolean value) { autoHide.set(value); }
248    public final boolean isAutoHide() { return autoHide.get(); }
249    public final BooleanProperty autoHideProperty() { return autoHide; }
250
251    /**
252     * Called after autoHide is run.
253     */
254    private ObjectProperty<EventHandler<Event>> onAutoHide =
255            new SimpleObjectProperty<EventHandler<Event>>(this, "onAutoHide");
256    public final void setOnAutoHide(EventHandler<Event> value) { onAutoHide.set(value); }
257    public final EventHandler<Event> getOnAutoHide() { return onAutoHide.get(); }
258    public final ObjectProperty<EventHandler<Event>> onAutoHideProperty() { return onAutoHide; }
259
260    /**
261     * Specifies whether the PopupWindow should be hidden when an unhandled escape key
262     * is pressed while the popup has focus.
263     * @defaultValue true
264     */
265    private BooleanProperty hideOnEscape =
266            new SimpleBooleanProperty(this, "hideOnEscape", true);
267    public final void setHideOnEscape(boolean value) { hideOnEscape.set(value); }
268    public final boolean isHideOnEscape() { return hideOnEscape.get(); }
269    public final BooleanProperty hideOnEscapeProperty() { return hideOnEscape; }
270
271    /**
272     * Specifies whether the event, which caused the Popup to hide, should be
273     * consumed. Having the event consumed prevents it from triggering some
274     * additional UI response in the Popup's owner window.
275     * @defaultValue true
276     * @since 2.2
277     */
278    private BooleanProperty consumeAutoHidingEvents =
279            new SimpleBooleanProperty(this, "consumeAutoHidingEvents",
280                                      true);
281
282    public final void setConsumeAutoHidingEvents(boolean value) {
283        consumeAutoHidingEvents.set(value);
284    }
285
286    public final boolean getConsumeAutoHidingEvents() {
287        return consumeAutoHidingEvents.get();
288    }
289
290    public final BooleanProperty consumeAutoHidingEventsProperty() {
291        return consumeAutoHidingEvents;
292    }
293
294    /**
295     * Show the popup.
296     * @param owner The owner of the popup. This must not be null.
297     * @throws NullPointerException if owner is null
298     * @throws IllegalArgumentException if the specified owner window would
299     *      create cycle in the window hierarchy
300     */
301    public void show(Window owner) {
302        validateOwnerWindow(owner);
303        showImpl(owner);
304    }
305
306    /**
307     * Shows the popup at the specified x,y location relative to the screen.
308     * The popup is associated with the specified owner node. The {@code Window}
309     * which contains the owner node at the time of the call becomes an owner
310     * window of the displayed popup.
311     * 
312     * @param ownerNode The owner Node of the popup. It must not be null
313     *        and must be associated with a Window.
314     * @param screenX the x location in screen coordinates at which to
315     *        show this PopupWindow.
316     * @param screenY the y location in screen coordiates at which to
317     *        show this PopupWindow.
318     * @throws NullPointerException if ownerNode is null
319     * @throws IllegalArgumentException if the specified owner node is not
320     *      associated with a Window or when the window would create cycle
321     *      in the window hierarchy
322     */
323    public void show(Node ownerNode, double screenX, double screenY) {
324        if (ownerNode == null) {
325            throw new NullPointerException("The owner node must not be null");
326        }
327
328        final Scene ownerNodeScene = ownerNode.getScene();
329        if ((ownerNodeScene == null)
330                || (ownerNodeScene.getWindow() == null)) {
331            throw new IllegalArgumentException(
332                    "The owner node needs to be associated with a window");
333        }
334
335        final Window newOwnerWindow = ownerNodeScene.getWindow();
336        validateOwnerWindow(newOwnerWindow);
337
338        this.ownerNode.set(ownerNode);
339        
340        // RT-28454 PopupWindow should disappear when owner node is not visible
341        if (ownerNode != null) { 
342            ownerNode.visibleProperty().addListener(ownerNodeListener);
343        }
344       
345        setX(screenX);
346        setY(screenY);
347        showImpl(newOwnerWindow);
348    }
349
350    /**
351     * Show the Popup at the specified x,y location relative to the screen
352     * @param ownerWindow The owner of the popup. This must not be null.
353     * @param screenX the x location in screen coordinates at which to
354     *        show this PopupWindow.
355     * @param screenY the y location in screen coordiates at which to
356     *        show this PopupWindow.
357     * @throws NullPointerException if ownerWindow is null
358     * @throws IllegalArgumentException if the specified owner window would
359     *      create cycle in the window hierarchy
360     */
361    public void show(Window ownerWindow, double screenX, double screenY) {
362        validateOwnerWindow(ownerWindow);
363
364        setX(screenX);
365        setY(screenY);
366        showImpl(ownerWindow);
367    }
368
369    private void showImpl(final Window owner) {
370        if (isShowing()) {
371            if (autofixHandler != null) {
372                autofixHandler.adjustPosition();
373            }
374            return;
375        }
376
377        // Update the owner field
378        this.ownerWindow.set(owner);
379        if (owner instanceof PopupWindow) {
380            ((PopupWindow)owner).children.add(this);
381        }
382        // RT-28454 PopupWindow should disappear when owner node is not visible
383        if (owner != null) {
384            owner.showingProperty().addListener(ownerNodeListener);
385        }
386
387        final Scene sceneValue = getScene();
388        if (sceneValue != null) {
389            SceneHelper.parentEffectiveOrientationInvalidated(sceneValue);            
390        }
391        
392        // RT-28447
393        final Scene ownerScene = getRootWindow(owner).getScene();
394        sceneValue.getStylesheets().setAll(ownerScene.getStylesheets());
395        
396        // It is required that the root window exist and be visible to show the popup.
397        if (getRootWindow(owner).isShowing()) {
398            // We do show() first so that the width and height of the
399            // popup window are initialized. This way the x,y location of the
400            // popup calculated below uses the right width and height values for
401            // its calculation. (fix for part of RT-10675).
402            show();
403        }
404    }
405
406    /**
407     * Hide this Popup and all its children
408     */
409    @Override public void hide() {
410        for (PopupWindow c : children) {
411            if (c.isShowing()) {
412                c.hide();
413            }
414        }        
415        children.clear();
416        super.hide();
417        // RT-28454 when popup hides, remove listeners; these are added when the popup shows.
418        if (getOwnerWindow() != null) getOwnerWindow().showingProperty().removeListener(ownerNodeListener);
419        if (getOwnerNode() != null) getOwnerNode().visibleProperty().removeListener(ownerNodeListener);
420    }
421
422    /**
423     * @treatAsPrivate implementation detail
424     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
425     */
426    @Deprecated
427    @Override protected void impl_visibleChanging(boolean visible) {
428        super.impl_visibleChanging(visible);
429        PerformanceTracker.logEvent("PopupWindow.storeVisible for [PopupWindow]");
430
431        Toolkit toolkit = Toolkit.getToolkit();
432        if (visible && (impl_peer == null)) {
433            // Setup the peer
434            impl_peer = toolkit.createTKPopupStage(StageStyle.TRANSPARENT, getOwnerWindow().impl_getPeer());
435            peerListener = new PopupWindowPeerListener(PopupWindow.this);
436        }
437    }
438
439    private Window rootWindow;
440
441    /**
442     * @treatAsPrivate implementation detail
443     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
444     */
445    @Deprecated
446    @Override protected void impl_visibleChanged(boolean visible) {
447        super.impl_visibleChanged(visible);
448
449        final Window ownerWindowValue = getOwnerWindow();
450        if (visible) {
451            rootWindow = getRootWindow(ownerWindowValue);
452
453            startMonitorOwnerEvents(ownerWindowValue);
454            // currently we consider popup window to be focused when it is
455            // visible and its owner window is focused (we need to track
456            // that through listener on owner window focused property)
457            // a better solution would require some focus manager, which can
458            // track focus state across multiple windows
459            bindOwnerFocusedProperty(ownerWindowValue);
460            setFocused(ownerWindowValue.isFocused());
461            handleAutofixActivation(true, isAutoFix());
462            rootWindow.increaseFocusGrabCounter();
463        } else {
464            stopMonitorOwnerEvents(ownerWindowValue);
465            unbindOwnerFocusedProperty(ownerWindowValue);
466            setFocused(false);
467            handleAutofixActivation(false, isAutoFix());
468            rootWindow.decreaseFocusGrabCounter();
469            rootWindow = null;
470        }
471
472        PerformanceTracker.logEvent("PopupWindow.storeVisible for [PopupWindow] finished");
473    }
474
475    private void syncWithRootBounds() {
476        final Parent rootNode = getScene().getRoot();
477        final Bounds layoutBounds = rootNode.getLayoutBounds();
478
479        final double layoutX = layoutBounds.getMinX();
480        final double layoutY = layoutBounds.getMinY();
481
482        // update popup dimensions
483        setWidth(layoutBounds.getMaxX() - layoutX);
484        setHeight(layoutBounds.getMaxY() - layoutY);
485        // update transform
486        rootNode.setTranslateX(-layoutX);
487        rootNode.setTranslateY(-layoutY);
488
489        if (isAlignWithContentOrigin()) {
490            // update window position
491            setWindowTranslate(layoutX, layoutY);
492            // compensate with scene's delta, so the manual Node.localToScene
493            // + sceenXY + windowXY calculation still works for local to screen
494            // conversions
495            SceneHelper.setSceneDelta(getScene(), layoutX, layoutY);
496
497            if (autofixActive) {
498                autofixHandler.adjustPosition();
499            }
500        }
501    }
502
503    /**
504     * Specifies the reference point associated with the x, y location of the
505     * window on the screen. If set to {@code false} this point corresponds to
506     * the window's upper left corner. If set to {@code true} the reference
507     * point is moved to the origin of the popup content coordinate space. This
508     * simplifies placement of popup windows which content have some additional
509     * borders extending past their origins. Setting the property to {code true}
510     * for such windows makes their position independent of their borders.
511     *
512     * @defaultValue {@code true}
513     * @since JavaFX 8
514     */
515    private BooleanProperty alignWithContentOrigin =
516            new BooleanPropertyBase(true) {
517                private boolean oldValue = true;
518
519                @Override
520                protected void invalidated() {
521                    final boolean newValue = get();
522                    if (oldValue != newValue) {
523                        if (newValue) {
524                            final Bounds layoutBounds =
525                                    getScene().getRoot().getLayoutBounds();
526                            setWindowTranslate(layoutBounds.getMinX(),
527                                               layoutBounds.getMinY());
528                            SceneHelper.setSceneDelta(getScene(),
529                                                      layoutBounds.getMinX(),
530                                                      layoutBounds.getMinY());
531                        } else {
532                            setWindowTranslate(0, 0);
533                            SceneHelper.setSceneDelta(getScene(), 0, 0);
534                        }
535
536                        if (autofixActive) {
537                            autofixHandler.adjustPosition();
538                        }
539
540                        oldValue = newValue;
541                    }
542                }
543
544                @Override
545                public Object getBean() {
546                    return PopupWindow.this;
547                }
548
549                @Override
550                public String getName() {
551                    return "alignWithContentOrigin";
552                }
553            };
554
555    public final void setAlignWithContentOrigin(boolean value) {
556        alignWithContentOrigin.set(value);
557    }
558
559    public final boolean isAlignWithContentOrigin() {
560        return alignWithContentOrigin.get();
561    }
562
563    public final BooleanProperty alignWithContentOriginProperty() {
564        return alignWithContentOrigin;
565    }
566
567    /**
568     *
569     * Gets the root (non PopupWindow) Window for the provided window.
570     *
571     * @param win the Window for which to get the root window
572     */
573    private static Window getRootWindow(Window win) {
574        // should be enough to traverse PopupWindow hierarchy here to get to the
575        // first non-popup focusable window
576        while (win instanceof PopupWindow) {
577            win = ((PopupWindow) win).getOwnerWindow();
578        }
579        return win;
580    }
581
582    void doAutoHide() {
583        // There is a timing problem here. I would like to have this isVisible
584        // check, such that we don't send an onAutoHide event if it was already
585        // invisible. However, visible is already false by the time this method
586        // gets called, when done by certain code paths.
587//        if (isVisible()) {
588        // hide this popup
589        hide();
590        if (getOnAutoHide() != null) {
591            getOnAutoHide().handle(new Event(this, this, Event.ANY));
592        }
593//        }
594    }
595
596    @Override
597    WindowEventDispatcher createInternalEventDispatcher() {
598        return new WindowEventDispatcher(new PopupEventRedirector(this),
599                                         new WindowCloseRequestHandler(this),
600                                         new EventHandlerManager(this));
601
602    }
603
604    @Override
605    Window getWindowOwner() {
606        return getOwnerWindow();
607    }
608
609    private void startMonitorOwnerEvents(final Window ownerWindowValue) {
610        final EventRedirector parentEventRedirector =
611                ownerWindowValue.getInternalEventDispatcher()
612                                .getEventRedirector();
613        parentEventRedirector.addEventDispatcher(getEventDispatcher());
614    }
615
616    private void stopMonitorOwnerEvents(final Window ownerWindowValue) {
617        final EventRedirector parentEventRedirector =
618                ownerWindowValue.getInternalEventDispatcher()
619                                .getEventRedirector();
620        parentEventRedirector.removeEventDispatcher(getEventDispatcher());
621    }
622
623    private ChangeListener<Boolean> ownerFocusedListener;
624
625    private void bindOwnerFocusedProperty(final Window ownerWindowValue) {
626        ownerFocusedListener =
627            new ChangeListener<Boolean>() {
628                @Override
629                public void changed(
630                        ObservableValue<? extends Boolean> observable,
631                        Boolean oldValue, Boolean newValue) {
632                    setFocused(newValue);
633                }
634            };
635        ownerWindowValue.focusedProperty().addListener(ownerFocusedListener);
636    }
637
638    private void unbindOwnerFocusedProperty(final Window ownerWindowValue) {
639        ownerWindowValue.focusedProperty().removeListener(ownerFocusedListener);
640        ownerFocusedListener = null;
641    }
642
643    private boolean autofixActive;
644    private AutofixHandler autofixHandler;
645    private void handleAutofixActivation(final boolean visible,
646                                         final boolean autofix) {
647        final boolean newAutofixActive = visible && autofix;
648        if (autofixActive != newAutofixActive) {
649            autofixActive = newAutofixActive;
650            if (newAutofixActive) {
651                autofixHandler = new AutofixHandler();
652                widthProperty().addListener(autofixHandler);
653                heightProperty().addListener(autofixHandler);
654                Screen.getScreens().addListener(autofixHandler);
655                autofixHandler.adjustPosition();
656            } else {
657                widthProperty().removeListener(autofixHandler);
658                heightProperty().removeListener(autofixHandler);
659                Screen.getScreens().removeListener(autofixHandler);
660                autofixHandler = null;
661            }
662        }
663    }
664
665    private void validateOwnerWindow(final Window owner) {
666        if (owner == null) {
667            throw new NullPointerException("Owner window must not be null");
668        }
669
670        if (wouldCreateCycle(owner, this)) {
671            throw new IllegalArgumentException(
672                    "Specified owner window would create cycle"
673                        + " in the window hierarchy");
674        }
675
676        if (isShowing() && (getOwnerWindow() != owner)) {
677            throw new IllegalStateException(
678                    "Popup is already shown with different owner window");
679        }
680    }
681
682    private static boolean wouldCreateCycle(Window parent, final Window child) {
683       while (parent != null) {
684           if (parent == child) {
685               return true;
686           }
687
688           parent = parent.getWindowOwner();
689       }
690
691       return false;
692    }
693
694    private final class AutofixHandler implements InvalidationListener {
695        @Override
696        public void invalidated(final Observable observable) {
697            adjustPosition();
698        }
699
700        public void adjustPosition() {
701            final Screen currentScreen =
702                    Utils.getScreenForPoint(getX(), getY());
703            final Rectangle2D screenBounds =
704                    Utils.hasFullScreenStage(currentScreen)
705                            ? currentScreen.getBounds()
706                            : currentScreen.getVisualBounds();
707            double wtX = getWindowTranslateX();
708            double wtY = getWindowTranslateY();
709            double oldWindowX = getX() + wtX;
710            double oldWindowY = getY() + wtY;
711            double _x = Math.min(oldWindowX,
712                                 screenBounds.getMaxX() - getWidth());
713            double _y = Math.min(oldWindowY,
714                                 screenBounds.getMaxY() - getHeight());
715            _x = Math.max(_x, screenBounds.getMinX());
716            _y = Math.max(_y, screenBounds.getMinY());
717            if (_x != oldWindowX) {
718                setX(_x - wtX);
719            }
720            if (_y != oldWindowY) {
721                setY(_y - wtY);
722            }
723        }
724    }
725
726    static class PopupEventRedirector extends EventRedirector {
727
728        private static final KeyCombination ESCAPE_KEY_COMBINATION =
729                KeyCombination.keyCombination("Esc");
730        private final PopupWindow popupWindow;
731
732        public PopupEventRedirector(final PopupWindow popupWindow) {
733            super(popupWindow);
734            this.popupWindow = popupWindow;
735        }
736
737        @Override
738        protected void handleRedirectedEvent(final Object eventSource,
739                final Event event) {
740            if (event instanceof KeyEvent) {
741                handleKeyEvent((KeyEvent) event);
742                return;
743            }
744
745            final EventType<?> eventType = event.getEventType();
746
747            if (eventType == MouseEvent.MOUSE_PRESSED) {
748                handleMousePressedEvent(eventSource, event);
749                return;
750            }
751
752            if (eventType == FocusUngrabEvent.FOCUS_UNGRAB) {
753                handleFocusUngrabEvent();
754                return;
755            }
756        }
757
758        private void handleKeyEvent(final KeyEvent event) {
759            if (event.isConsumed()) {
760                return;
761            }
762
763            final Scene scene = popupWindow.getScene();
764            if (scene != null) {
765                final Node sceneFocusOwner = scene.getFocusOwner();
766                final EventTarget eventTarget =
767                        (sceneFocusOwner != null) ? sceneFocusOwner : scene;
768                if (EventUtil.fireEvent(eventTarget, new DirectEvent(event))
769                        == null) {
770                    event.consume();
771                    return;
772                }
773            }
774
775            if ((event.getEventType() == KeyEvent.KEY_PRESSED)
776                    && ESCAPE_KEY_COMBINATION.match(event)) {
777                handleEscapeKeyPressedEvent(event);
778            }
779        }
780
781        private void handleEscapeKeyPressedEvent(final Event event) {
782            if (popupWindow.isHideOnEscape()) {
783                popupWindow.doAutoHide();
784
785                if (popupWindow.getConsumeAutoHidingEvents()) {
786                    event.consume();
787                }
788            }
789        }
790
791        private void handleMousePressedEvent(final Object eventSource,
792                final Event event) {
793            // we handle mouse pressed only for the immediate parent window,
794            // where we can check whether the mouse press is inside of the owner
795            // control or not, we will force possible child popups to close
796            // by sending the FOCUS_UNGRAB event
797            if (popupWindow.getOwnerWindow() != eventSource) {
798                return;
799            }
800
801            if (popupWindow.isAutoHide() && !isOwnerNodeEvent(event)) {
802                // the mouse press is outside of the owner control,
803                // fire FOCUS_UNGRAB to child popups
804                Event.fireEvent(popupWindow, new FocusUngrabEvent());
805
806                popupWindow.doAutoHide();
807
808                if (popupWindow.getConsumeAutoHidingEvents()) {
809                    event.consume();
810                }
811            }
812        }
813
814        private void handleFocusUngrabEvent() {
815            if (popupWindow.isAutoHide()) {
816                popupWindow.doAutoHide();
817            }
818        }
819
820        private boolean isOwnerNodeEvent(final Event event) {
821            final Node ownerNode = popupWindow.getOwnerNode();
822            if (ownerNode == null) {
823                return false;
824            }
825
826            final EventTarget eventTarget = event.getTarget();
827            if (!(eventTarget instanceof Node)) {
828                return false;
829            }
830
831            Node node = (Node) eventTarget;
832            do {
833                if (node == ownerNode) {
834                    return true;
835                }
836                node = node.getParent();
837            } while (node != null);
838
839            return false;
840        }
841    }
842}