1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.ContextMenuContent;
  29 import com.sun.javafx.scene.control.EmbeddedTextContextMenuContent;
  30 import com.sun.javafx.scene.control.Properties;
  31 import com.sun.javafx.scene.control.skin.Utils;
  32 import javafx.event.Event;
  33 import javafx.event.EventHandler;
  34 import javafx.scene.AccessibleAttribute;
  35 import javafx.scene.Node;
  36 import javafx.scene.control.ContextMenu;
  37 import javafx.scene.control.Control;
  38 import javafx.scene.control.Menu;
  39 import javafx.scene.control.Skin;
  40 import javafx.scene.input.KeyCode;
  41 import javafx.scene.input.KeyEvent;
  42 import javafx.scene.layout.Region;
  43 import javafx.stage.WindowEvent;
  44 import com.sun.javafx.scene.control.behavior.TwoLevelFocusPopupBehavior;
  45 
  46 /**
  47  * Default Skin implementation for ContextMenu. Several controls use ContextMenu in
  48  * order to display items in a drop down. This class mostly deals mostly with
  49  * show / hide logic - the actual content of the context menu is contained within
  50  * the {@link #getNode() root node}.
  51  *
  52  * @since 9
  53  * @see ContextMenu
  54  */
  55 public class ContextMenuSkin implements Skin<ContextMenu> {
  56 
  57     /***************************************************************************
  58      *                                                                         *
  59      * Private fields                                                          *
  60      *                                                                         *
  61      **************************************************************************/
  62 
  63     /* need to hold a reference to popupMenu here because getSkinnable() deliberately
  64      * returns null in PopupControlSkin. */
  65     private ContextMenu popupMenu;
  66     
  67     private final Region root;
  68     private TwoLevelFocusPopupBehavior tlFocus;
  69 
  70 
  71 
  72     /***************************************************************************
  73      *                                                                         *
  74      * Listeners                                                               *
  75      *                                                                         *
  76      **************************************************************************/
  77     
  78     // Fix for RT-18247
  79     private final EventHandler<KeyEvent> keyListener = new EventHandler<KeyEvent>() {
  80         @Override public void handle(KeyEvent event) {
  81             if (event.getEventType() != KeyEvent.KEY_PRESSED) return;
  82             
  83             // We only care if the root container still has focus
  84             if (! root.isFocused()) return;
  85             
  86             final KeyCode code = event.getCode();
  87             switch (code) {
  88                 case ENTER: 
  89                 case SPACE: popupMenu.hide(); return;
  90                 default:    return;
  91             }
  92         }
  93     };
  94 
  95 
  96 
  97     /***************************************************************************
  98      *                                                                         *
  99      * Constructors                                                            *
 100      *                                                                         *
 101      **************************************************************************/
 102 
 103     /**
 104      * Creates a new ContextMenuSkin instance.
 105      *
 106      * @param control The control that this skin should be installed onto.
 107      */
 108     public ContextMenuSkin(final ContextMenu control) {
 109         this.popupMenu = control;
 110         
 111         // When a contextMenu is shown, requestFocus on its content to enable
 112         // keyboard navigation.
 113         popupMenu.addEventHandler(Menu.ON_SHOWN, new EventHandler<Event>() {
 114             @Override public void handle(Event event) {
 115                 Node cmContent = popupMenu.getSkin().getNode();
 116                 if (cmContent != null) {
 117                     cmContent.requestFocus();
 118                     if (cmContent instanceof ContextMenuContent) {
 119                         Node accMenu = ((ContextMenuContent)cmContent).getItemsContainer();
 120                         accMenu.notifyAccessibleAttributeChanged(AccessibleAttribute.VISIBLE);
 121                     }
 122                 }
 123                 
 124                 root.addEventHandler(KeyEvent.KEY_PRESSED, keyListener);
 125             }
 126         });
 127         popupMenu.addEventHandler(Menu.ON_HIDDEN, new EventHandler<Event>() {
 128             @Override public void handle(Event event) {
 129                 Node cmContent = popupMenu.getSkin().getNode();
 130                 if (cmContent != null) cmContent.requestFocus();
 131                 
 132                 root.removeEventHandler(KeyEvent.KEY_PRESSED, keyListener);
 133             }
 134         });
 135 
 136         // For accessibility Menu.ON_HIDING does not work because isShowing is true
 137         // during the event, Menu.ON_HIDDEN does not work because the Window (in glass)
 138         // has already being disposed. The fix is to use WINDOW_HIDING (WINDOW_HIDDEN).
 139         popupMenu.addEventFilter(WindowEvent.WINDOW_HIDING, new EventHandler<Event>() {
 140             @Override public void handle(Event event) {
 141                 Node cmContent = popupMenu.getSkin().getNode();
 142                 if (cmContent instanceof ContextMenuContent) {
 143                     Node accMenu = ((ContextMenuContent)cmContent).getItemsContainer();
 144                     accMenu.notifyAccessibleAttributeChanged(AccessibleAttribute.VISIBLE);
 145                 }
 146             }
 147         });
 148 
 149         if (Properties.IS_TOUCH_SUPPORTED &&
 150             popupMenu.getStyleClass().contains("text-input-context-menu")) {
 151             root = new EmbeddedTextContextMenuContent(popupMenu);
 152         } else {
 153             root = new ContextMenuContent(popupMenu);
 154         }
 155         root.idProperty().bind(popupMenu.idProperty());
 156         root.styleProperty().bind(popupMenu.styleProperty());
 157         root.getStyleClass().addAll(popupMenu.getStyleClass()); // TODO needs to handle updates
 158 
 159         // Only add this if we're on an embedded platform that supports 5-button navigation
 160         if (Utils.isTwoLevelFocus()) {
 161             tlFocus = new TwoLevelFocusPopupBehavior(popupMenu); // needs to be last.
 162         }
 163     }
 164 
 165 
 166 
 167     /***************************************************************************
 168      *                                                                         *
 169      * Public API                                                              *
 170      *                                                                         *
 171      **************************************************************************/
 172 
 173     /** {@inheritDoc} */
 174     @Override public ContextMenu getSkinnable() {
 175         return popupMenu;
 176     }
 177 
 178     /** {@inheritDoc} */
 179     @Override public Node getNode() {
 180         return root;
 181     }
 182 
 183     /** {@inheritDoc} */
 184     @Override public void dispose() {
 185         root.idProperty().unbind();
 186         root.styleProperty().unbind();
 187         if (tlFocus != null) tlFocus.dispose();
 188     }
 189 }