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