Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.control;
027
028import javafx.beans.property.ObjectProperty;
029import javafx.beans.property.ObjectPropertyBase;
030import javafx.collections.ListChangeListener.Change;
031import javafx.collections.ObservableList;
032import javafx.event.Event;
033import javafx.event.EventHandler;
034import javafx.event.EventType;
035import javafx.scene.Node;
036
037import com.sun.javafx.collections.TrackableObservableList;
038import com.sun.javafx.scene.control.Logging;
039import javafx.beans.DefaultProperty;
040import javafx.beans.property.ReadOnlyBooleanProperty;
041import javafx.beans.property.ReadOnlyBooleanWrapper;
042import javafx.event.EventDispatchChain;
043
044/**
045 * <p>
046 * A popup menu of actionable items which is displayed to the user only upon request.
047 * When a menu is visible, in most use cases, the user can select one menu item
048 * before the menu goes back to its hidden state. This means the menu is a good
049 * place to put important functionality that does not necessarily need to be
050 * visible at all times to the user.
051 * <p>
052 * Menus are typically placed in a {@link MenuBar}, or as a submenu of another Menu.
053 * If the intention is to offer a context menu when the user right-clicks in a
054 * certain area of their user interface, then this is the wrong control to use.
055 * This is because when Menu is added to the scenegraph, it has a visual
056 * representation that will result in it appearing on screen. Instead,
057 * {@link ContextMenu} should be used in this circumstance.
058 * <p>
059 * Creating a Menu and inserting it into a MenuBar is easy, as shown below:
060 * <pre><code>
061 * final Menu menu1 = new Menu("File");
062 * MenuBar menuBar = new MenuBar();
063 * menuBar.getMenus().add(menu1);
064 * </code></pre>
065 * <p>
066 * A Menu is a subclass of {@link MenuItem} which means that it can be inserted
067 * into a Menu's {@link #items} ObservableList, resulting in a submenu being created:
068 * <pre><code>
069 * MenuItem menu12 = new MenuItem("Open");
070 * menu1.getItems().add(menu12);
071 * </code></pre>
072 * <p>
073 * The items ObservableList allows for any {@link MenuItem} type to be inserted,
074 * including its subclasses {@link Menu}, {@link MenuItem}, {@link RadioMenuItem}, {@link CheckMenuItem},
075 * {@link CustomMenuItem} and {@link SeparatorMenuItem}. In order to insert an arbitrary {@link Node} to
076 * a Menu, a CustomMenuItem can be used. One exception to this general rule is that
077 * {@link SeparatorMenuItem} could be used for inserting a separator.
078 *
079 * @see MenuBar
080 * @see MenuItem
081 */
082@DefaultProperty("items")
083public class Menu extends MenuItem {
084
085    /**
086     * <p>Called when the contentMenu for this menu <b>will</b> be shown. However if the
087     * contextMenu is empty then this will not be called.
088     * </p>
089     */
090    public static final EventType<Event> ON_SHOWING =
091            new EventType<Event>(Event.ANY, "MENU_ON_SHOWING");
092
093    /**
094     * <p>Called when the contentMenu for this menu shows. However if the
095     * contextMenu is empty then this will not be called.
096     * </p>
097     */
098    public static final EventType<Event> ON_SHOWN =
099            new EventType<Event>(Event.ANY, "MENU_ON_SHOWN");
100
101    /**
102     * <p>Called when the contentMenu for this menu <b>will</b> be hidden. However if the
103     * contextMenu is empty then this will not be called.
104     * </p>
105     */
106    public static final EventType<Event> ON_HIDING =
107            new EventType<Event>(Event.ANY, "MENU_ON_HIDING");
108
109    /**
110     * <p>Called when the contentMenu for this menu is hidden. However if the
111     * contextMenu is empty then this will not be called.
112     * </p>
113     */
114    public static final EventType<Event> ON_HIDDEN =
115            new EventType<Event>(Event.ANY, "MENU_ON_HIDDEN");
116
117    /***************************************************************************
118     *                                                                         *
119     * Constructors                                                            *
120     *                                                                         *
121     **************************************************************************/
122
123    /**
124     * Constructs a Menu with an empty string for its display text.
125     * @since 2.2
126     */
127    public Menu() {
128        this("");
129    }
130    
131    /**
132     * Constructs a Menu and sets the display text with the specified text.
133     */
134    public Menu(String text) {
135        this(text,null);
136    }
137
138    /**
139     * Constructs a Menu and sets the display text with the specified text
140     * and sets the graphic {@link Node} to the given node.
141     */
142    public Menu(String text, Node graphic) {
143        super(text,graphic);
144        getStyleClass().add(DEFAULT_STYLE_CLASS);
145    }
146
147
148
149     /***************************************************************************
150     *                                                                         *
151     * Properties                                                              *
152     *                                                                         *
153     **************************************************************************/
154
155    /**
156     * Indicates whether the {@link ContextMenu} is currently visible.
157     *
158     * @defaultValue false
159     */
160    private ReadOnlyBooleanWrapper showing;
161    
162    private void setShowing(boolean value) {
163        if (getItems().size() == 0 || (value && isShowing())) return;
164        
165        // these events will not fire if the showing property is bound
166        if (value) {
167           if (getOnMenuValidation() != null) {
168                Event.fireEvent(this, new Event(MENU_VALIDATION_EVENT));
169                for(MenuItem m : getItems()) {
170                    if (!(m instanceof Menu) && m.getOnMenuValidation() != null) {
171                        Event.fireEvent(m, new Event(MenuItem.MENU_VALIDATION_EVENT));
172                    }
173                }
174           }
175           Event.fireEvent(this, new Event(Menu.ON_SHOWING));
176        } else {
177           Event.fireEvent(this, new Event(Menu.ON_HIDING));
178        }
179        showingPropertyImpl().set(value);
180        Event.fireEvent(this, (value) ? new Event(Menu.ON_SHOWN) : 
181            new Event(Menu.ON_HIDDEN));
182    }
183
184    public final boolean isShowing() {
185        return showing == null ? false : showing.get();
186    }
187
188    public final ReadOnlyBooleanProperty showingProperty() {
189        return showingPropertyImpl().getReadOnlyProperty();
190    }
191
192    private ReadOnlyBooleanWrapper showingPropertyImpl() {
193        if (showing == null) {
194            showing = new ReadOnlyBooleanWrapper() {
195                @Override protected void invalidated() {
196                    // force validation
197                    get();
198                    
199                    // update the styleclass
200                    if (isShowing()) {
201                        getStyleClass().add(STYLE_CLASS_SHOWING);
202                    } else {
203                        getStyleClass().remove(STYLE_CLASS_SHOWING);
204                    }
205                }
206
207                @Override
208                public Object getBean() {
209                    return Menu.this;
210                }
211
212                @Override
213                public String getName() {
214                    return "showing";
215                }
216            };
217        }
218        return showing;
219    }
220
221
222    // --- On Showing
223    public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; }
224    /**
225     * Called just prior to the {@code ContextMenu} being shown, even if the menu has
226     * no items to show. Note however that this won't be called if the menu does
227     * not have a valid anchor node.
228     */
229    public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); }
230    public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); }
231    private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() {
232        @Override protected void invalidated() {
233            eventHandlerManager.setEventHandler(ON_SHOWING, get());
234         }
235
236         @Override
237         public Object getBean() {
238             return Menu.this;
239         }
240
241         @Override
242         public String getName() {
243             return "onShowing";
244         }
245     };
246
247
248    // -- On Shown
249    public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; }
250    /**
251     * Called just after the {@link ContextMenu} is shown.
252     */
253    public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); }
254    public final EventHandler<Event> getOnShown() { return onShownProperty().get(); }
255    private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() {
256        @Override protected void invalidated() {
257            eventHandlerManager.setEventHandler(ON_SHOWN, get());
258        }
259
260        @Override
261        public Object getBean() {
262            return Menu.this;
263        }
264
265        @Override
266        public String getName() {
267            return "onShown";
268        }
269    };
270
271
272    // --- On Hiding
273    public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; }
274    /**
275     * Called just prior to the {@link ContextMenu} being hidden.
276     */
277    public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); }
278    public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); }
279    private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() {
280        @Override protected void invalidated() {
281            eventHandlerManager.setEventHandler(ON_HIDING, get());
282        }
283
284        @Override
285        public Object getBean() {
286            return Menu.this;
287        }
288
289        @Override
290        public String getName() {
291            return "onHiding";
292        }
293    };
294
295
296    // --- On Hidden
297    public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; }
298    /**
299     * Called just after the {@link ContextMenu} has been hidden.
300     */
301    public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); }
302    public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); }
303    private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() {
304        @Override protected void invalidated() {
305            eventHandlerManager.setEventHandler(ON_HIDDEN, get());
306        }
307
308        @Override
309        public Object getBean() {
310            return Menu.this;
311        }
312
313        @Override
314        public String getName() {
315            return "onHidden";
316        }
317    };
318
319
320
321    /***************************************************************************
322     *                                                                         *
323     * Instance variables                                                      *
324     *                                                                         *
325     **************************************************************************/
326
327    private final ObservableList<MenuItem> items = new TrackableObservableList<MenuItem>() {
328        @Override protected void onChanged(Change<MenuItem> c) {
329            while (c.next()) {
330                // remove the parent menu from all menu items that have been removed
331                for (MenuItem item : c.getRemoved()) {
332                    item.setParentMenu(null);
333                }
334
335                // set the parent menu to be this menu for all added menu items
336                for (MenuItem item : c.getAddedSubList()) {
337                    if (item.getParentMenu() != null) {
338                        Logging.getControlsLogger().warning("Adding MenuItem " +
339                                item.getText() + " that has already been added to "
340                                + item.getParentMenu().getText());
341                        item.getParentMenu().getItems().remove(item);
342                    }
343
344                    item.setParentMenu(Menu.this);
345                }
346            }
347            if (getItems().size() == 0 && isShowing()) {
348                showingPropertyImpl().set(false);
349            }
350        }
351    };
352
353
354
355    /***************************************************************************
356     *                                                                         *
357     * Public API                                                              *
358     *                                                                         *
359     **************************************************************************/
360
361    /**
362     * The items to show within this menu. If this ObservableList is modified at
363     * runtime, the Menu will update as expected.
364     */
365    public final ObservableList<MenuItem> getItems() {
366        return items;
367    }
368    
369    /**
370     * If the Menu is not disabled and the {@link ContextMenu} is not already showing,
371     * then this will cause the {@link ContextMenu} to be shown.
372     */
373    public void show() {
374        if (isDisable()) return;
375        setShowing(true);
376    }
377
378    /**
379     * Hides the {@link ContextMenu} if it was previously showing, and any showing
380     * submenus. If this menu is not showing, then invoking this function
381     * has no effect.
382     */
383    public void hide() {
384        if (!isShowing()) return;
385        // hide all sub menus
386        for (MenuItem i : getItems()) {
387            if (i instanceof Menu) {
388                final Menu m = (Menu) i;
389                m.hide();
390            }
391        }
392        setShowing(false);
393    }
394    
395    @Override public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
396        eventHandlerManager.addEventHandler(eventType, eventHandler);
397    }
398    
399    @Override public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
400        eventHandlerManager.removeEventHandler(eventType, eventHandler);
401    }
402
403     /** {@inheritDoc} */
404    @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
405        return tail.prepend(eventHandlerManager);
406    }
407
408    /***************************************************************************
409     *                                                                         *
410     * Stylesheet Handling                                                     *
411     *                                                                         *
412     **************************************************************************/
413
414    private static final String DEFAULT_STYLE_CLASS = "menu";
415    private static final String STYLE_CLASS_SHOWING = "showing";
416}