1 /* 2 * Copyright (c) 2010, 2019, 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 com.sun.javafx.beans.IDProperty; 29 import javafx.beans.property.ObjectProperty; 30 import javafx.beans.property.ObjectPropertyBase; 31 import javafx.collections.ListChangeListener.Change; 32 import javafx.collections.ObservableList; 33 import javafx.event.ActionEvent; 34 import javafx.event.Event; 35 import javafx.event.EventHandler; 36 import javafx.geometry.HPos; 37 import javafx.geometry.Point2D; 38 import javafx.geometry.Side; 39 import javafx.geometry.VPos; 40 import javafx.scene.Node; 41 import javafx.scene.Scene; 42 import javafx.stage.Window; 43 44 import com.sun.javafx.util.Utils; 45 import com.sun.javafx.collections.TrackableObservableList; 46 import javafx.scene.control.skin.ContextMenuSkin; 47 import javafx.beans.property.BooleanProperty; 48 import javafx.beans.property.SimpleBooleanProperty; 49 50 /** 51 * <p> 52 * A popup control containing an ObservableList of menu items. The {@link #getItems() items} 53 * ObservableList allows for any {@link MenuItem} type to be inserted, 54 * including its subclasses {@link Menu}, {@link MenuItem}, {@link RadioMenuItem}, {@link CheckMenuItem} and 55 * {@link CustomMenuItem}. If an arbitrary Node needs to be 56 * inserted into a menu, a CustomMenuItem can be used. One exception to this general rule is that 57 * {@link SeparatorMenuItem} could be used for inserting a separator. 58 * <p> 59 * A common use case for this class is creating and showing context menus to 60 * users. To create a context menu using ContextMenu you can do the 61 * following: 62 * 63 <pre><code>final ContextMenu contextMenu = new ContextMenu(); 64 contextMenu.setOnShowing(new EventHandler<WindowEvent>() { 65 public void handle(WindowEvent e) { 66 System.out.println("showing"); 67 } 68 }); 69 contextMenu.setOnShown(new EventHandler<WindowEvent>() { 70 public void handle(WindowEvent e) { 71 System.out.println("shown"); 72 } 73 }); 74 75 MenuItem item1 = new MenuItem("About"); 76 item1.setOnAction(new EventHandler<ActionEvent>() { 77 public void handle(ActionEvent e) { 78 System.out.println("About"); 79 } 80 }); 81 MenuItem item2 = new MenuItem("Preferences"); 82 item2.setOnAction(new EventHandler<ActionEvent>() { 83 public void handle(ActionEvent e) { 84 System.out.println("Preferences"); 85 } 86 }); 87 contextMenu.getItems().addAll(item1, item2); 88 89 final TextField textField = new TextField("Type Something"); 90 textField.setContextMenu(contextMenu);</code></pre> 91 * 92 * <img src="doc-files/ContextMenu.png" alt="Image of the ContextMenu control"> 93 * 94 * <p>{@link Control#setContextMenu(javafx.scene.control.ContextMenu) } convenience 95 * method can be used to set a context menu on on any control. The example above results in the 96 * context menu being displayed on the right {@link javafx.geometry.Side Side} 97 * of the TextField. Alternatively, an event handler can also be set on the control 98 * to invoke the context menu as shown below. 99 * 100 <pre><code>textField.setOnAction(new EventHandler<ActionEvent>() { 101 public void handle(ActionEvent e) { 102 contextMenu.show(textField, Side.BOTTOM, 0, 0); 103 } 104 }); 105 106 Group root = (Group) scene.getRoot(); 107 root.getChildren().add(textField); 108 </code></pre> 109 * 110 * <p>In this example, the context menu is shown when the user clicks on the 111 * {@link javafx.scene.control.Button Button} (of course, you should use the 112 * {@link MenuButton} control to do this rather than doing the above).</p> 113 * 114 * <p>Note that the show function used in the code sample 115 * above will result in the ContextMenu appearing directly beneath the 116 * TextField. You can vary the {@link javafx.geometry.Side Side} to get the results you expect.</p> 117 * 118 * @see MenuItem 119 * @see Menu 120 * @since JavaFX 2.0 121 */ 122 @IDProperty("id") 123 public class ContextMenu extends PopupControl { 124 125 /*************************************************************************** 126 * * 127 * Fields * 128 * * 129 **************************************************************************/ 130 131 private boolean showRelativeToWindow = false; 132 133 134 135 /*************************************************************************** 136 * * 137 * Constructors * 138 * * 139 **************************************************************************/ 140 141 /** 142 * Create a new ContextMenu 143 */ 144 public ContextMenu() { 145 getStyleClass().setAll(DEFAULT_STYLE_CLASS); 146 setAutoHide(true); 147 setConsumeAutoHidingEvents(false); 148 } 149 150 /** 151 * Create a new ContextMenu initialized with the given items 152 * @param items the list of menu items 153 */ 154 public ContextMenu(MenuItem... items) { 155 this(); 156 this.items.addAll(items); 157 } 158 159 160 161 /*************************************************************************** 162 * * 163 * Properties * 164 * * 165 **************************************************************************/ 166 167 /** 168 * Callback function to be informed when an item contained within this 169 * {@code ContextMenu} has been activated. The current implementation informs 170 * all parent menus as well, so that it is not necessary to listen to all 171 * sub menus for events. 172 */ 173 private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() { 174 @Override protected void invalidated() { 175 setEventHandler(ActionEvent.ACTION, get()); 176 } 177 178 @Override 179 public Object getBean() { 180 return ContextMenu.this; 181 } 182 183 @Override 184 public String getName() { 185 return "onAction"; 186 } 187 }; 188 public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); } 189 public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); } 190 public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; } 191 192 private final ObservableList<MenuItem> items = new TrackableObservableList<MenuItem>() { 193 @Override protected void onChanged(Change<MenuItem> c) { 194 while (c.next()) { 195 for (MenuItem item : c.getRemoved()) { 196 item.setParentPopup(null); 197 } 198 for (MenuItem item : c.getAddedSubList()) { 199 if (item.getParentPopup() != null) { 200 // we need to remove this item from its current parentPopup 201 // as a MenuItem should not exist in multiple parentPopup 202 // instances 203 item.getParentPopup().getItems().remove(item); 204 } 205 item.setParentPopup(ContextMenu.this); 206 } 207 } 208 } 209 }; 210 211 212 213 /*************************************************************************** 214 * * 215 * Public API * 216 * * 217 **************************************************************************/ 218 219 /** 220 * The menu items on the context menu. If this ObservableList is modified at 221 * runtime, the ContextMenu will update as expected. 222 * @return the menu items on this context menu 223 * @see MenuItem 224 */ 225 public final ObservableList<MenuItem> getItems() { return items; } 226 227 /** 228 * Shows the {@code ContextMenu} relative to the given anchor node, on the side 229 * specified by the {@code hpos} and {@code vpos} parameters, and offset 230 * by the given {@code dx} and {@code dy} values for the x-axis and y-axis, respectively. 231 * If there is not enough room, the menu is moved to the opposite side and 232 * the offset is not applied. 233 * <p> 234 * To clarify the purpose of the {@code hpos} and {@code vpos} parameters, 235 * consider that they are relative to the anchor node. As such, a {@code hpos} 236 * and {@code vpos} of {@code CENTER} would mean that the ContextMenu appears 237 * on top of the anchor, with the (0,0) position of the {@code ContextMenu} 238 * positioned at (0,0) of the anchor. A {@code hpos} of right would then shift 239 * the {@code ContextMenu} such that its top-left (0,0) position would be attached 240 * to the top-right position of the anchor. 241 * <p> 242 * This function is useful for finely tuning the position of a menu, 243 * relative to the parent node to ensure close alignment. 244 * @param anchor the anchor node 245 * @param side the side 246 * @param dx the dx value for the x-axis 247 * @param dy the dy value for the y-axis 248 */ 249 // TODO provide more detail 250 public void show(Node anchor, Side side, double dx, double dy) { 251 if (anchor == null) return; 252 if (getItems().size() == 0) return; 253 254 getScene().setNodeOrientation(anchor.getEffectiveNodeOrientation()); 255 // FIXME because Side is not yet in javafx.geometry, we have to convert 256 // to the old HPos/VPos API here, as Utils can not refer to Side in the 257 // charting API. 258 HPos hpos = side == Side.LEFT ? HPos.LEFT : side == Side.RIGHT ? HPos.RIGHT : HPos.CENTER; 259 VPos vpos = side == Side.TOP ? VPos.TOP : side == Side.BOTTOM ? VPos.BOTTOM : VPos.CENTER; 260 261 // translate from anchor/hpos/vpos/dx/dy into screenX/screenY 262 Point2D point = Utils.pointRelativeTo(anchor, 263 prefWidth(-1), prefHeight(-1), 264 hpos, vpos, dx, dy, true); 265 doShow(anchor, point.getX(), point.getY()); 266 } 267 268 /** 269 * Shows the {@code ContextMenu} at the specified screen coordinates. If there 270 * is not enough room at the specified location to show the {@code ContextMenu} 271 * given its size requirements, the necessary adjustments are made to bring 272 * the {@code ContextMenu} back back on screen. This also means that the 273 * {@code ContextMenu} will not span multiple monitors. 274 * @param anchor the anchor node 275 * @param screenX the x position of the anchor in screen coordinates 276 * @param screenY the y position of the anchor in screen coordinates 277 */ 278 @Override 279 public void show(Node anchor, double screenX, double screenY) { 280 if (anchor == null) return; 281 if (getItems().size() == 0) return; 282 getScene().setNodeOrientation(anchor.getEffectiveNodeOrientation()); 283 doShow(anchor, screenX, screenY); 284 } 285 286 /** 287 * Hides this {@code ContextMenu} and any visible submenus, assuming that when this function 288 * is called that the {@code ContextMenu} was showing. 289 * <p> 290 * If this {@code ContextMenu} is not showing, then nothing happens. 291 */ 292 @Override public void hide() { 293 if (!isShowing()) return; 294 Event.fireEvent(this, new Event(Menu.ON_HIDING)); 295 super.hide(); 296 Event.fireEvent(this, new Event(Menu.ON_HIDDEN)); 297 } 298 299 /** {@inheritDoc} */ 300 @Override protected Skin<?> createDefaultSkin() { 301 return new ContextMenuSkin(this); 302 } 303 304 305 306 /*************************************************************************** 307 * * 308 * Private Implementation * 309 * * 310 **************************************************************************/ 311 312 final boolean isShowRelativeToWindow() { return showRelativeToWindow; } 313 final void setShowRelativeToWindow(boolean value) { showRelativeToWindow = value; } 314 315 private void doShow(Node anchor, double screenX, double screenY) { 316 Event.fireEvent(this, new Event(Menu.ON_SHOWING)); 317 if(isShowRelativeToWindow()) { 318 final Scene scene = (anchor == null) ? null : anchor.getScene(); 319 final Window win = (scene == null) ? null : scene.getWindow(); 320 if (win == null) return; 321 super.show(win, screenX, screenY); 322 } else { 323 super.show(anchor, screenX, screenY); 324 } 325 Event.fireEvent(this, new Event(Menu.ON_SHOWN)); 326 } 327 328 329 330 /*************************************************************************** 331 * * 332 * Stylesheet Handling * 333 * * 334 ***************************************************************************/ 335 336 private static final String DEFAULT_STYLE_CLASS = "context-menu"; 337 }