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&lt;WindowEvent&gt;() {
  65     public void handle(WindowEvent e) {
  66         System.out.println("showing");
  67     }
  68 });
  69 contextMenu.setOnShown(new EventHandler&lt;WindowEvent&gt;() {
  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&lt;ActionEvent&gt;() {
  77     public void handle(ActionEvent e) {
  78         System.out.println("About");
  79     }
  80 });
  81 MenuItem item2 = new MenuItem("Preferences");
  82 item2.setOnAction(new EventHandler&lt;ActionEvent&gt;() {
  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&lt;ActionEvent&gt;() {
 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 }