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 com.sun.javafx.scene.control.skin;
  27 
  28 import javafx.event.Event;
  29 import javafx.event.EventHandler;
  30 import javafx.scene.AccessibleAttribute;
  31 import javafx.scene.Node;
  32 import javafx.scene.control.ContextMenu;
  33 import javafx.scene.control.Menu;
  34 import javafx.scene.control.Skin;
  35 import javafx.scene.input.KeyCode;
  36 import javafx.scene.input.KeyEvent;
  37 import javafx.scene.layout.Region;
  38 import javafx.stage.WindowEvent;
  39 import com.sun.javafx.scene.control.behavior.TwoLevelFocusPopupBehavior;
  40 
  41 /**
  42  * Default Skin implementation for PopupMenu. Several controls use PopupMenu in
  43  * order to display items in a drop down. It deals mostly with show hide logic for
  44  * Popup based controls, and specifically in this case for PopupMenu. This is
  45  * explained in the superclass {@link PopupControlSkin}.
  46  * <p>
  47  * Region/css based skin implementation for PopupMenu is the {@link PopupMenuContent}
  48  * class.
  49  */
  50 public class ContextMenuSkin implements Skin<ContextMenu> {
  51 
  52     /* need to hold a reference to popupMenu here because getSkinnable() deliberately
  53      * returns null in PopupControlSkin. */
  54     private ContextMenu popupMenu;
  55     
  56     private final Region root;
  57     private TwoLevelFocusPopupBehavior tlFocus;
  58     
  59     // Fix for RT-18247
  60     private final EventHandler<KeyEvent> keyListener = new EventHandler<KeyEvent>() {
  61         @Override public void handle(KeyEvent event) {
  62             if (event.getEventType() != KeyEvent.KEY_PRESSED) return;
  63             
  64             // We only care if the root container still has focus
  65             if (! root.isFocused()) return;
  66             
  67             final KeyCode code = event.getCode();
  68             switch (code) {
  69                 case ENTER: 
  70                 case SPACE: popupMenu.hide(); return;
  71                 default:    return;
  72             }
  73         }
  74     };
  75 
  76     /***/
  77     public ContextMenuSkin(final ContextMenu popupMenu) {
  78         this.popupMenu = popupMenu;
  79         
  80         // When a contextMenu is shown, requestFocus on its content to enable
  81         // keyboard navigation.
  82         popupMenu.addEventHandler(Menu.ON_SHOWN, new EventHandler<Event>() {
  83             @Override public void handle(Event event) {
  84                 Node cmContent = popupMenu.getSkin().getNode();
  85                 if (cmContent != null) {
  86                     cmContent.requestFocus();
  87                     if (cmContent instanceof ContextMenuContent) {
  88                         Node accMenu = ((ContextMenuContent)cmContent).getItemsContainer();
  89                         accMenu.notifyAccessibleAttributeChanged(AccessibleAttribute.VISIBLE);
  90                     }
  91                 }
  92                 
  93                 root.addEventHandler(KeyEvent.KEY_PRESSED, keyListener);
  94             }
  95         });
  96         popupMenu.addEventHandler(Menu.ON_HIDDEN, new EventHandler<Event>() {
  97             @Override public void handle(Event event) {
  98                 Node cmContent = popupMenu.getSkin().getNode();
  99                 if (cmContent != null) cmContent.requestFocus();
 100                 
 101                 root.removeEventHandler(KeyEvent.KEY_PRESSED, keyListener);
 102             }
 103         });
 104 
 105         // For accessibility Menu.ON_HIDING does not work because isShowing is true
 106         // during the event, Menu.ON_HIDDEN does not work because the Window (in glass)
 107         // has already being disposed. The fix is to use WINDOW_HIDING (WINDOW_HIDDEN).
 108         popupMenu.addEventFilter(WindowEvent.WINDOW_HIDING, new EventHandler<Event>() {
 109             @Override public void handle(Event event) {
 110                 Node cmContent = popupMenu.getSkin().getNode();
 111                 if (cmContent instanceof ContextMenuContent) {
 112                     Node accMenu = ((ContextMenuContent)cmContent).getItemsContainer();
 113                     accMenu.notifyAccessibleAttributeChanged(AccessibleAttribute.VISIBLE);
 114                 }
 115             }
 116         });
 117 
 118         if (BehaviorSkinBase.IS_TOUCH_SUPPORTED &&
 119             popupMenu.getStyleClass().contains("text-input-context-menu")) {
 120             root = new EmbeddedTextContextMenuContent(popupMenu);
 121         } else {
 122             root = new ContextMenuContent(popupMenu);
 123         }
 124         root.idProperty().bind(popupMenu.idProperty());
 125         root.styleProperty().bind(popupMenu.styleProperty());
 126         root.getStyleClass().addAll(popupMenu.getStyleClass()); // TODO needs to handle updates
 127 
 128         // Only add this if we're on an embedded platform that supports 5-button navigation
 129         if (Utils.isTwoLevelFocus()) {
 130             tlFocus = new TwoLevelFocusPopupBehavior(popupMenu); // needs to be last.
 131         }
 132     }
 133 
 134     @Override public ContextMenu getSkinnable() {
 135         return popupMenu;
 136     }
 137 
 138     @Override public Node getNode() {
 139         return root;
 140     }
 141 
 142     @Override public void dispose() {
 143         root.idProperty().unbind();
 144         root.styleProperty().unbind();
 145         if (tlFocus != null) tlFocus.dispose();
 146     }
 147 }