1 /* 2 * Copyright (c) 2010, 2016, 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.NodeHelper; 29 import com.sun.javafx.scene.control.ContextMenuContent; 30 import com.sun.javafx.scene.control.ControlAcceleratorSupport; 31 import com.sun.javafx.scene.control.LabeledImpl; 32 import com.sun.javafx.scene.control.skin.Utils; 33 import javafx.application.Platform; 34 import javafx.collections.ListChangeListener; 35 import javafx.event.ActionEvent; 36 import javafx.scene.Node; 37 import javafx.scene.control.ContextMenu; 38 import javafx.scene.control.MenuButton; 39 import javafx.scene.control.MenuItem; 40 import javafx.scene.control.Skin; 41 import javafx.scene.control.SkinBase; 42 import javafx.scene.input.MouseEvent; 43 import javafx.scene.layout.Region; 44 import javafx.scene.layout.StackPane; 45 import com.sun.javafx.scene.control.behavior.MenuButtonBehaviorBase; 46 47 /** 48 * Base class for MenuButtonSkin and SplitMenuButtonSkin. It consists of the 49 * label, the arrowButton with its arrow shape, and the popup. 50 * 51 * @since 9 52 */ 53 public class MenuButtonSkinBase<C extends MenuButton> extends SkinBase<C> { 54 55 /*************************************************************************** 56 * * 57 * Private fields * 58 * * 59 **************************************************************************/ 60 61 final LabeledImpl label; 62 final StackPane arrow; 63 final StackPane arrowButton; 64 ContextMenu popup; 65 66 /** 67 * If true, the control should behave like a button for mouse button events. 68 */ 69 boolean behaveLikeButton = false; 70 private ListChangeListener<MenuItem> itemsChangedListener; 71 72 73 74 /*************************************************************************** 75 * * 76 * Constructors * 77 * * 78 **************************************************************************/ 79 80 /** 81 * Creates a new instance of MenuButtonSkinBase, although note that this 82 * instance does not handle any behavior / input mappings - this needs to be 83 * handled appropriately by subclasses. 84 * 85 * @param control The control that this skin should be installed onto. 86 */ 87 public MenuButtonSkinBase(final C control) { 88 super(control); 89 90 if (control.getOnMousePressed() == null) { 91 control.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { 92 MenuButtonBehaviorBase behavior = getBehavior(); 93 if (behavior != null) { 94 behavior.mousePressed(e, behaveLikeButton); 95 } 96 }); 97 } 98 99 if (control.getOnMouseReleased() == null) { 100 control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { 101 MenuButtonBehaviorBase behavior = getBehavior(); 102 if (behavior != null) { 103 behavior.mouseReleased(e, behaveLikeButton); 104 } 105 }); 106 } 107 108 /* 109 * Create the objects we will be displaying. 110 */ 111 label = new MenuLabeledImpl(getSkinnable()); 112 label.setMnemonicParsing(control.isMnemonicParsing()); 113 label.setLabelFor(control); 114 115 arrow = new StackPane(); 116 arrow.getStyleClass().setAll("arrow"); 117 arrow.setMaxWidth(Region.USE_PREF_SIZE); 118 arrow.setMaxHeight(Region.USE_PREF_SIZE); 119 120 arrowButton = new StackPane(); 121 arrowButton.getStyleClass().setAll("arrow-button"); 122 arrowButton.getChildren().add(arrow); 123 124 popup = new ContextMenu(); 125 popup.getItems().clear(); 126 popup.getItems().addAll(getSkinnable().getItems()); 127 128 getChildren().clear(); 129 getChildren().addAll(label, arrowButton); 130 131 getSkinnable().requestLayout(); 132 133 itemsChangedListener = c -> { 134 while (c.next()) { 135 popup.getItems().removeAll(c.getRemoved()); 136 popup.getItems().addAll(c.getFrom(), c.getAddedSubList()); 137 } 138 }; 139 control.getItems().addListener(itemsChangedListener); 140 141 if (getSkinnable().getScene() != null) { 142 ControlAcceleratorSupport.addAcceleratorsIntoScene(getSkinnable().getItems(), getSkinnable()); 143 } 144 control.sceneProperty().addListener((scene, oldValue, newValue) -> { 145 if (getSkinnable() != null && getSkinnable().getScene() != null) { 146 ControlAcceleratorSupport.addAcceleratorsIntoScene(getSkinnable().getItems(), getSkinnable()); 147 } 148 }); 149 150 // Register listeners 151 registerChangeListener(control.showingProperty(), e -> { 152 if (getSkinnable().isShowing()) { 153 show(); 154 } else { 155 hide(); 156 } 157 }); 158 registerChangeListener(control.focusedProperty(), e -> { 159 // Handle tabbing away from an open MenuButton 160 if (!getSkinnable().isFocused() && getSkinnable().isShowing()) { 161 hide(); 162 } 163 if (!getSkinnable().isFocused() && popup.isShowing()) { 164 hide(); 165 } 166 }); 167 registerChangeListener(control.mnemonicParsingProperty(), e -> { 168 label.setMnemonicParsing(getSkinnable().isMnemonicParsing()); 169 getSkinnable().requestLayout(); 170 }); 171 registerChangeListener(popup.showingProperty(), e -> { 172 if (!popup.isShowing() && getSkinnable().isShowing()) { 173 // Popup was dismissed. Maybe user clicked outside or typed ESCAPE. 174 // Make sure button is in sync. 175 getSkinnable().hide(); 176 } 177 178 if (popup.isShowing()) { 179 Utils.addMnemonics(popup, getSkinnable().getScene(), NodeHelper.isShowMnemonics(getSkinnable())); 180 } else { 181 // we wrap this in a runLater so that mnemonics are not removed 182 // before all key events are fired (because KEY_PRESSED might have 183 // been used to hide the menu, but KEY_TYPED and KEY_RELEASED 184 // events are still to be fired, and shouldn't miss out on going 185 // through the mnemonics code (especially in case they should be 186 // consumed to prevent them being used elsewhere). 187 // See JBS-8090026 for more detail. 188 Platform.runLater(() -> Utils.removeMnemonics(popup, getSkinnable().getScene())); 189 } 190 }); 191 } 192 193 194 195 /*************************************************************************** 196 * * 197 * Private implementation * 198 * * 199 **************************************************************************/ 200 201 /** {@inheritDoc} */ 202 @Override public void dispose() { 203 getSkinnable().getItems().removeListener(itemsChangedListener); 204 super.dispose(); 205 if (popup != null ) { 206 if (popup.getSkin() != null && popup.getSkin().getNode() != null) { 207 ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode(); 208 cmContent.dispose(); 209 } 210 popup.setSkin(null); 211 popup = null; 212 } 213 } 214 215 /** {@inheritDoc} */ 216 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 217 return leftInset 218 + label.minWidth(height) 219 + snapSizeX(arrowButton.minWidth(height)) 220 + rightInset; 221 } 222 223 /** {@inheritDoc} */ 224 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 225 return topInset 226 + Math.max(label.minHeight(width), snapSizeY(arrowButton.minHeight(-1))) 227 + bottomInset; 228 } 229 230 /** {@inheritDoc} */ 231 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 232 return leftInset 233 + label.prefWidth(height) 234 + snapSizeX(arrowButton.prefWidth(height)) 235 + rightInset; 236 } 237 238 /** {@inheritDoc} */ 239 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 240 return topInset 241 + Math.max(label.prefHeight(width), snapSizeY(arrowButton.prefHeight(-1))) 242 + bottomInset; 243 } 244 245 /** {@inheritDoc} */ 246 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 247 return getSkinnable().prefWidth(height); 248 } 249 250 /** {@inheritDoc} */ 251 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 252 return getSkinnable().prefHeight(width); 253 } 254 255 /** {@inheritDoc} */ 256 @Override protected void layoutChildren(final double x, final double y, 257 final double w, final double h) { 258 final double arrowButtonWidth = snapSizeX(arrowButton.prefWidth(-1)); 259 label.resizeRelocate(x, y, w - arrowButtonWidth, h); 260 arrowButton.resizeRelocate(x + (w - arrowButtonWidth), y, arrowButtonWidth, h); 261 } 262 263 264 265 /*************************************************************************** 266 * * 267 * Private implementation * 268 * * 269 **************************************************************************/ 270 271 MenuButtonBehaviorBase<C> getBehavior() { 272 return null; 273 } 274 275 private void show() { 276 if (!popup.isShowing()) { 277 popup.show(getSkinnable(), getSkinnable().getPopupSide(), 0, 0); 278 } 279 } 280 281 private void hide() { 282 if (popup.isShowing()) { 283 popup.hide(); 284 } 285 } 286 287 boolean requestFocusOnFirstMenuItem = false; 288 void requestFocusOnFirstMenuItem() { 289 this.requestFocusOnFirstMenuItem = true; 290 } 291 292 void putFocusOnFirstMenuItem() { 293 Skin<?> popupSkin = popup.getSkin(); 294 if (popupSkin instanceof ContextMenuSkin) { 295 Node node = popupSkin.getNode(); 296 if (node instanceof ContextMenuContent) { 297 ((ContextMenuContent)node).requestFocusOnIndex(0); 298 } 299 } 300 } 301 302 303 304 /*************************************************************************** 305 * * 306 * Support classes * 307 * * 308 **************************************************************************/ 309 310 private static class MenuLabeledImpl extends LabeledImpl { 311 MenuButton button; 312 public MenuLabeledImpl(MenuButton b) { 313 super(b); 314 button = b; 315 addEventHandler(ActionEvent.ACTION, e -> { 316 button.fireEvent(new ActionEvent()); 317 e.consume(); 318 }); 319 } 320 } 321 }