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