1 /* 2 * Copyright (c) 2010, 2017, 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; 27 28 import javafx.beans.property.ObjectProperty; 29 import javafx.beans.property.ObjectPropertyBase; 30 import javafx.collections.ListChangeListener.Change; 31 import javafx.collections.ObservableList; 32 import javafx.event.Event; 33 import javafx.event.EventHandler; 34 import javafx.event.EventType; 35 import javafx.scene.Node; 36 37 import com.sun.javafx.collections.TrackableObservableList; 38 import com.sun.javafx.scene.control.Logging; 39 import javafx.beans.DefaultProperty; 40 import javafx.beans.property.ReadOnlyBooleanProperty; 41 import javafx.beans.property.ReadOnlyBooleanWrapper; 42 import javafx.event.EventDispatchChain; 43 44 /** 45 * <p> 46 * A popup menu of actionable items which is displayed to the user only upon request. 47 * When a menu is visible, in most use cases, the user can select one menu item 48 * before the menu goes back to its hidden state. This means the menu is a good 49 * place to put important functionality that does not necessarily need to be 50 * visible at all times to the user. 51 * <p> 52 * Menus are typically placed in a {@link MenuBar}, or as a submenu of another Menu. 53 * If the intention is to offer a context menu when the user right-clicks in a 54 * certain area of their user interface, then this is the wrong control to use. 55 * This is because when Menu is added to the scenegraph, it has a visual 56 * representation that will result in it appearing on screen. Instead, 57 * {@link ContextMenu} should be used in this circumstance. 58 * <p> 59 * Creating a Menu and inserting it into a MenuBar is easy, as shown below: 60 * <pre><code> 61 * final Menu menu1 = new Menu("File"); 62 * MenuBar menuBar = new MenuBar(); 63 * menuBar.getMenus().add(menu1); 64 * </code></pre> 65 * <p> 66 * A Menu is a subclass of {@link MenuItem} which means that it can be inserted 67 * into a Menu's {@link #getItems() items} ObservableList, resulting in a submenu being created: 68 * <pre><code> 69 * MenuItem menu12 = new MenuItem("Open"); 70 * menu1.getItems().add(menu12); 71 * </code></pre> 72 * <p> 73 * The items ObservableList allows for any {@link MenuItem} type to be inserted, 74 * including its subclasses {@link Menu}, {@link MenuItem}, {@link RadioMenuItem}, {@link CheckMenuItem}, 75 * {@link CustomMenuItem} and {@link SeparatorMenuItem}. In order to insert an arbitrary {@link Node} to 76 * a Menu, a CustomMenuItem can be used. One exception to this general rule is that 77 * {@link SeparatorMenuItem} could be used for inserting a separator. 78 * 79 * @see MenuBar 80 * @see MenuItem 81 * @since JavaFX 2.0 82 */ 83 @DefaultProperty("items") 84 public class Menu extends MenuItem { 85 86 /** 87 * <p>Called when the contextMenu for this menu <b>will</b> be shown. However if the 88 * contextMenu is empty then this will not be called. 89 * </p> 90 */ 91 public static final EventType<Event> ON_SHOWING = 92 new EventType<Event>(Event.ANY, "MENU_ON_SHOWING"); 93 94 /** 95 * <p>Called when the contextMenu for this menu shows. However if the 96 * contextMenu is empty then this will not be called. 97 * </p> 98 */ 99 public static final EventType<Event> ON_SHOWN = 100 new EventType<Event>(Event.ANY, "MENU_ON_SHOWN"); 101 102 /** 103 * <p>Called when the contextMenu for this menu <b>will</b> be hidden. However if the 104 * contextMenu is empty then this will not be called. 105 * </p> 106 */ 107 public static final EventType<Event> ON_HIDING = 108 new EventType<Event>(Event.ANY, "MENU_ON_HIDING"); 109 110 /** 111 * <p>Called when the contextMenu for this menu is hidden. However if the 112 * contextMenu is empty then this will not be called. 113 * </p> 114 */ 115 public static final EventType<Event> ON_HIDDEN = 116 new EventType<Event>(Event.ANY, "MENU_ON_HIDDEN"); 117 118 /*************************************************************************** 119 * * 120 * Constructors * 121 * * 122 **************************************************************************/ 123 124 /** 125 * Constructs a Menu with an empty string for its display text. 126 * @since JavaFX 2.2 127 */ 128 public Menu() { 129 this(""); 130 } 131 132 /** 133 * Constructs a Menu and sets the display text with the specified text. 134 * 135 * @param text the text to display on the menu button 136 */ 137 public Menu(String text) { 138 this(text,null); 139 } 140 141 /** 142 * Constructs a Menu and sets the display text with the specified text 143 * and sets the graphic {@link Node} to the given node. 144 * 145 * @param text the text to display on the menu button 146 * @param graphic the graphic to display on the menu button 147 */ 148 public Menu(String text, Node graphic) { 149 this(text, graphic, (MenuItem[])null); 150 } 151 152 /** 153 * Constructs a Menu and sets the display text with the specified text, 154 * the graphic {@link Node} to the given node, and inserts the given items 155 * into the {@link #getItems() items} list. 156 * 157 * @param text the text to display on the menu button 158 * @param graphic the graphic to display on the menu button 159 * @param items The items to display in the popup menu. 160 * @since JavaFX 8u40 161 */ 162 public Menu(String text, Node graphic, MenuItem... items) { 163 super(text,graphic); 164 getStyleClass().add(DEFAULT_STYLE_CLASS); 165 166 if (items != null) { 167 getItems().addAll(items); 168 } 169 170 parentPopupProperty().addListener(observable -> { 171 for (int i = 0; i < getItems().size(); i++) { 172 MenuItem item = getItems().get(i); 173 item.setParentPopup(getParentPopup()); 174 } 175 }); 176 } 177 178 179 180 /*************************************************************************** 181 * * 182 * Properties * 183 * * 184 **************************************************************************/ 185 186 /** 187 * Indicates whether the {@link ContextMenu} is currently visible. 188 * 189 * @defaultValue false 190 */ 191 private ReadOnlyBooleanWrapper showing; 192 193 private void setShowing(boolean value) { 194 if (getItems().size() == 0 || (value && isShowing())) return; 195 196 // these events will not fire if the showing property is bound 197 if (value) { 198 if (getOnMenuValidation() != null) { 199 Event.fireEvent(this, new Event(MENU_VALIDATION_EVENT)); 200 for(MenuItem m : getItems()) { 201 if (!(m instanceof Menu) && m.getOnMenuValidation() != null) { 202 Event.fireEvent(m, new Event(MenuItem.MENU_VALIDATION_EVENT)); 203 } 204 } 205 } 206 Event.fireEvent(this, new Event(Menu.ON_SHOWING)); 207 } else { 208 Event.fireEvent(this, new Event(Menu.ON_HIDING)); 209 } 210 showingPropertyImpl().set(value); 211 Event.fireEvent(this, (value) ? new Event(Menu.ON_SHOWN) : 212 new Event(Menu.ON_HIDDEN)); 213 } 214 215 public final boolean isShowing() { 216 return showing == null ? false : showing.get(); 217 } 218 219 public final ReadOnlyBooleanProperty showingProperty() { 220 return showingPropertyImpl().getReadOnlyProperty(); 221 } 222 223 private ReadOnlyBooleanWrapper showingPropertyImpl() { 224 if (showing == null) { 225 showing = new ReadOnlyBooleanWrapper() { 226 @Override protected void invalidated() { 227 // force validation 228 get(); 229 230 // update the styleclass 231 if (isShowing()) { 232 getStyleClass().add(STYLE_CLASS_SHOWING); 233 } else { 234 getStyleClass().remove(STYLE_CLASS_SHOWING); 235 } 236 } 237 238 @Override 239 public Object getBean() { 240 return Menu.this; 241 } 242 243 @Override 244 public String getName() { 245 return "showing"; 246 } 247 }; 248 } 249 return showing; 250 } 251 252 253 // --- On Showing 254 /** 255 * Called just prior to the {@code ContextMenu} being shown, even if the menu has 256 * no items to show. Note however that this won't be called if the menu does 257 * not have a valid anchor node. 258 * @return the on showing property 259 */ 260 public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; } 261 public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); } 262 public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); } 263 private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() { 264 @Override protected void invalidated() { 265 eventHandlerManager.setEventHandler(ON_SHOWING, get()); 266 } 267 268 @Override 269 public Object getBean() { 270 return Menu.this; 271 } 272 273 @Override 274 public String getName() { 275 return "onShowing"; 276 } 277 }; 278 279 280 // -- On Shown 281 /** 282 * Called just after the {@link ContextMenu} is shown. 283 * @return the on shown property 284 */ 285 public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; } 286 public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); } 287 public final EventHandler<Event> getOnShown() { return onShownProperty().get(); } 288 private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() { 289 @Override protected void invalidated() { 290 eventHandlerManager.setEventHandler(ON_SHOWN, get()); 291 } 292 293 @Override 294 public Object getBean() { 295 return Menu.this; 296 } 297 298 @Override 299 public String getName() { 300 return "onShown"; 301 } 302 }; 303 304 305 // --- On Hiding 306 /** 307 * Called just prior to the {@link ContextMenu} being hidden. 308 * @return the on hiding property 309 */ 310 public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; } 311 public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); } 312 public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); } 313 private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() { 314 @Override protected void invalidated() { 315 eventHandlerManager.setEventHandler(ON_HIDING, get()); 316 } 317 318 @Override 319 public Object getBean() { 320 return Menu.this; 321 } 322 323 @Override 324 public String getName() { 325 return "onHiding"; 326 } 327 }; 328 329 330 // --- On Hidden 331 /** 332 * Called just after the {@link ContextMenu} has been hidden. 333 * @return the on hidden property 334 */ 335 public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; } 336 public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); } 337 public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); } 338 private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() { 339 @Override protected void invalidated() { 340 eventHandlerManager.setEventHandler(ON_HIDDEN, get()); 341 } 342 343 @Override 344 public Object getBean() { 345 return Menu.this; 346 } 347 348 @Override 349 public String getName() { 350 return "onHidden"; 351 } 352 }; 353 354 355 356 /*************************************************************************** 357 * * 358 * Instance variables * 359 * * 360 **************************************************************************/ 361 362 private final ObservableList<MenuItem> items = new TrackableObservableList<MenuItem>() { 363 @Override protected void onChanged(Change<MenuItem> c) { 364 while (c.next()) { 365 // remove the parent menu from all menu items that have been removed 366 for (MenuItem item : c.getRemoved()) { 367 item.setParentMenu(null); 368 item.setParentPopup(null); 369 } 370 371 // set the parent menu to be this menu for all added menu items 372 for (MenuItem item : c.getAddedSubList()) { 373 if (item.getParentMenu() != null) { 374 Logging.getControlsLogger().warning("Adding MenuItem " + 375 item.getText() + " that has already been added to " 376 + item.getParentMenu().getText()); 377 item.getParentMenu().getItems().remove(item); 378 } 379 380 item.setParentMenu(Menu.this); 381 item.setParentPopup(getParentPopup()); 382 } 383 } 384 if (getItems().size() == 0 && isShowing()) { 385 showingPropertyImpl().set(false); 386 } 387 } 388 }; 389 390 391 392 /*************************************************************************** 393 * * 394 * Public API * 395 * * 396 **************************************************************************/ 397 398 /** 399 * The items to show within this menu. If this ObservableList is modified at 400 * runtime, the Menu will update as expected. 401 * @return the list of items 402 */ 403 public final ObservableList<MenuItem> getItems() { 404 return items; 405 } 406 407 /** 408 * If the Menu is not disabled and the {@link ContextMenu} is not already showing, 409 * then this will cause the {@link ContextMenu} to be shown. 410 */ 411 public void show() { 412 if (isDisable()) return; 413 setShowing(true); 414 } 415 416 /** 417 * Hides the {@link ContextMenu} if it was previously showing, and any showing 418 * submenus. If this menu is not showing, then invoking this function 419 * has no effect. 420 */ 421 public void hide() { 422 if (!isShowing()) return; 423 // hide all sub menus 424 for (MenuItem i : getItems()) { 425 if (i instanceof Menu) { 426 final Menu m = (Menu) i; 427 m.hide(); 428 } 429 } 430 setShowing(false); 431 } 432 433 /** {@inheritDoc} */ 434 @Override public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) { 435 eventHandlerManager.addEventHandler(eventType, eventHandler); 436 } 437 438 /** {@inheritDoc} */ 439 @Override public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) { 440 eventHandlerManager.removeEventHandler(eventType, eventHandler); 441 } 442 443 /** {@inheritDoc} */ 444 @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { 445 return tail.prepend(eventHandlerManager); 446 } 447 448 /*************************************************************************** 449 * * 450 * Stylesheet Handling * 451 * * 452 **************************************************************************/ 453 454 private static final String DEFAULT_STYLE_CLASS = "menu"; 455 private static final String STYLE_CLASS_SHOWING = "showing"; 456 }