/*
* Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javafx.scene.control;
import javafx.beans.property.
ObjectProperty;
import javafx.beans.property.
ObjectPropertyBase;
import javafx.collections.
ListChangeListener.
Change;
import javafx.collections.
ObservableList;
import javafx.event.
Event;
import javafx.event.
EventHandler;
import javafx.event.
EventType;
import javafx.scene.
Node;
import com.sun.javafx.collections.
TrackableObservableList;
import com.sun.javafx.scene.control.
Logging;
import javafx.beans.
DefaultProperty;
import javafx.beans.property.
ReadOnlyBooleanProperty;
import javafx.beans.property.
ReadOnlyBooleanWrapper;
import javafx.event.
EventDispatchChain;
/**
* <p>
* A popup menu of actionable items which is displayed to the user only upon request.
* When a menu is visible, in most use cases, the user can select one menu item
* before the menu goes back to its hidden state. This means the menu is a good
* place to put important functionality that does not necessarily need to be
* visible at all times to the user.
* <p>
* Menus are typically placed in a {@link MenuBar}, or as a submenu of another Menu.
* If the intention is to offer a context menu when the user right-clicks in a
* certain area of their user interface, then this is the wrong control to use.
* This is because when Menu is added to the scenegraph, it has a visual
* representation that will result in it appearing on screen. Instead,
* {@link ContextMenu} should be used in this circumstance.
* <p>
* Creating a Menu and inserting it into a MenuBar is easy, as shown below:
* <pre><code>
* final Menu menu1 = new Menu("File");
* MenuBar menuBar = new MenuBar();
* menuBar.getMenus().add(menu1);
* </code></pre>
* <p>
* A Menu is a subclass of {@link MenuItem} which means that it can be inserted
* into a Menu's {@link #items} ObservableList, resulting in a submenu being created:
* <pre><code>
* MenuItem menu12 = new MenuItem("Open");
* menu1.getItems().add(menu12);
* </code></pre>
* <p>
* The items ObservableList allows for any {@link MenuItem} type to be inserted,
* including its subclasses {@link Menu}, {@link MenuItem}, {@link RadioMenuItem}, {@link CheckMenuItem},
* {@link CustomMenuItem} and {@link SeparatorMenuItem}. In order to insert an arbitrary {@link Node} to
* a Menu, a CustomMenuItem can be used. One exception to this general rule is that
* {@link SeparatorMenuItem} could be used for inserting a separator.
*
* @see MenuBar
* @see MenuItem
* @since JavaFX 2.0
*/
@
DefaultProperty("items")
public class
Menu extends
MenuItem {
/**
* <p>Called when the contextMenu for this menu <b>will</b> be shown. However if the
* contextMenu is empty then this will not be called.
* </p>
*/
public static final
EventType<
Event>
ON_SHOWING =
new
EventType<
Event>(
Event.
ANY, "MENU_ON_SHOWING");
/**
* <p>Called when the contextMenu for this menu shows. However if the
* contextMenu is empty then this will not be called.
* </p>
*/
public static final
EventType<
Event>
ON_SHOWN =
new
EventType<
Event>(
Event.
ANY, "MENU_ON_SHOWN");
/**
* <p>Called when the contextMenu for this menu <b>will</b> be hidden. However if the
* contextMenu is empty then this will not be called.
* </p>
*/
public static final
EventType<
Event>
ON_HIDING =
new
EventType<
Event>(
Event.
ANY, "MENU_ON_HIDING");
/**
* <p>Called when the contextMenu for this menu is hidden. However if the
* contextMenu is empty then this will not be called.
* </p>
*/
public static final
EventType<
Event>
ON_HIDDEN =
new
EventType<
Event>(
Event.
ANY, "MENU_ON_HIDDEN");
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Constructs a Menu with an empty string for its display text.
* @since JavaFX 2.2
*/
public
Menu() {
this("");
}
/**
* Constructs a Menu and sets the display text with the specified text.
*
* @param text the text to display on the menu button
*/
public
Menu(
String text) {
this(
text,null);
}
/**
* Constructs a Menu and sets the display text with the specified text
* and sets the graphic {@link Node} to the given node.
*
* @param text the text to display on the menu button
* @param graphic the graphic to display on the menu button
*/
public
Menu(
String text,
Node graphic) {
this(
text,
graphic, (
MenuItem[])null);
}
/**
* Constructs a Menu and sets the display text with the specified text,
* the graphic {@link Node} to the given node, and inserts the given items
* into the {@link #getItems() items} list.
*
* @param text the text to display on the menu button
* @param graphic the graphic to display on the menu button
* @param items The items to display in the popup menu.
* @since JavaFX 8u40
*/
public
Menu(
String text,
Node graphic,
MenuItem...
items) {
super(
text,
graphic);
getStyleClass().
add(
DEFAULT_STYLE_CLASS);
if (
items != null) {
getItems().
addAll(
items);
}
parentPopupProperty().
addListener(
observable -> {
for (int
i = 0;
i <
getItems().
size();
i++) {
MenuItem item =
getItems().
get(
i);
item.
setParentPopup(
getParentPopup());
}
});
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* Indicates whether the {@link ContextMenu} is currently visible.
*
* @defaultValue false
*/
private
ReadOnlyBooleanWrapper showing;
private void
setShowing(boolean
value) {
if (
getItems().
size() == 0 || (
value &&
isShowing())) return;
// these events will not fire if the showing property is bound
if (
value) {
if (
getOnMenuValidation() != null) {
Event.
fireEvent(this, new
Event(
MENU_VALIDATION_EVENT));
for(
MenuItem m :
getItems()) {
if (!(
m instanceof
Menu) &&
m.
getOnMenuValidation() != null) {
Event.
fireEvent(
m, new
Event(
MenuItem.
MENU_VALIDATION_EVENT));
}
}
}
Event.
fireEvent(this, new
Event(
Menu.
ON_SHOWING));
} else {
Event.
fireEvent(this, new
Event(
Menu.
ON_HIDING));
}
showingPropertyImpl().
set(
value);
Event.
fireEvent(this, (
value) ? new
Event(
Menu.
ON_SHOWN) :
new
Event(
Menu.
ON_HIDDEN));
}
public final boolean
isShowing() {
return
showing == null ? false :
showing.
get();
}
public final
ReadOnlyBooleanProperty showingProperty() {
return
showingPropertyImpl().
getReadOnlyProperty();
}
private
ReadOnlyBooleanWrapper showingPropertyImpl() {
if (
showing == null) {
showing = new
ReadOnlyBooleanWrapper() {
@
Override protected void
invalidated() {
// force validation
get();
// update the styleclass
if (
isShowing()) {
getStyleClass().
add(
STYLE_CLASS_SHOWING);
} else {
getStyleClass().
remove(
STYLE_CLASS_SHOWING);
}
}
@
Override
public
Object getBean() {
return
Menu.this;
}
@
Override
public
String getName() {
return "showing";
}
};
}
return
showing;
}
// --- On Showing
/**
* Called just prior to the {@code ContextMenu} being shown, even if the menu has
* no items to show. Note however that this won't be called if the menu does
* not have a valid anchor node.
*/
public final
ObjectProperty<
EventHandler<
Event>>
onShowingProperty() { return
onShowing; }
public final void
setOnShowing(
EventHandler<
Event>
value) {
onShowingProperty().
set(
value); }
public final
EventHandler<
Event>
getOnShowing() { return
onShowingProperty().
get(); }
private
ObjectProperty<
EventHandler<
Event>>
onShowing = new
ObjectPropertyBase<
EventHandler<
Event>>() {
@
Override protected void
invalidated() {
eventHandlerManager.
setEventHandler(
ON_SHOWING,
get());
}
@
Override
public
Object getBean() {
return
Menu.this;
}
@
Override
public
String getName() {
return "onShowing";
}
};
// -- On Shown
/**
* Called just after the {@link ContextMenu} is shown.
*/
public final
ObjectProperty<
EventHandler<
Event>>
onShownProperty() { return
onShown; }
public final void
setOnShown(
EventHandler<
Event>
value) {
onShownProperty().
set(
value); }
public final
EventHandler<
Event>
getOnShown() { return
onShownProperty().
get(); }
private
ObjectProperty<
EventHandler<
Event>>
onShown = new
ObjectPropertyBase<
EventHandler<
Event>>() {
@
Override protected void
invalidated() {
eventHandlerManager.
setEventHandler(
ON_SHOWN,
get());
}
@
Override
public
Object getBean() {
return
Menu.this;
}
@
Override
public
String getName() {
return "onShown";
}
};
// --- On Hiding
/**
* Called just prior to the {@link ContextMenu} being hidden.
*/
public final
ObjectProperty<
EventHandler<
Event>>
onHidingProperty() { return
onHiding; }
public final void
setOnHiding(
EventHandler<
Event>
value) {
onHidingProperty().
set(
value); }
public final
EventHandler<
Event>
getOnHiding() { return
onHidingProperty().
get(); }
private
ObjectProperty<
EventHandler<
Event>>
onHiding = new
ObjectPropertyBase<
EventHandler<
Event>>() {
@
Override protected void
invalidated() {
eventHandlerManager.
setEventHandler(
ON_HIDING,
get());
}
@
Override
public
Object getBean() {
return
Menu.this;
}
@
Override
public
String getName() {
return "onHiding";
}
};
// --- On Hidden
/**
* Called just after the {@link ContextMenu} has been hidden.
*/
public final
ObjectProperty<
EventHandler<
Event>>
onHiddenProperty() { return
onHidden; }
public final void
setOnHidden(
EventHandler<
Event>
value) {
onHiddenProperty().
set(
value); }
public final
EventHandler<
Event>
getOnHidden() { return
onHiddenProperty().
get(); }
private
ObjectProperty<
EventHandler<
Event>>
onHidden = new
ObjectPropertyBase<
EventHandler<
Event>>() {
@
Override protected void
invalidated() {
eventHandlerManager.
setEventHandler(
ON_HIDDEN,
get());
}
@
Override
public
Object getBean() {
return
Menu.this;
}
@
Override
public
String getName() {
return "onHidden";
}
};
/***************************************************************************
* *
* Instance variables *
* *
**************************************************************************/
private final
ObservableList<
MenuItem>
items = new
TrackableObservableList<
MenuItem>() {
@
Override protected void
onChanged(
Change<
MenuItem>
c) {
while (
c.
next()) {
// remove the parent menu from all menu items that have been removed
for (
MenuItem item :
c.
getRemoved()) {
item.
setParentMenu(null);
item.
setParentPopup(null);
}
// set the parent menu to be this menu for all added menu items
for (
MenuItem item :
c.
getAddedSubList()) {
if (
item.
getParentMenu() != null) {
Logging.
getControlsLogger().
warning("Adding MenuItem " +
item.
getText() + " that has already been added to "
+
item.
getParentMenu().
getText());
item.
getParentMenu().
getItems().
remove(
item);
}
item.
setParentMenu(
Menu.this);
item.
setParentPopup(
getParentPopup());
}
}
if (
getItems().
size() == 0 &&
isShowing()) {
showingPropertyImpl().
set(false);
}
}
};
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
/**
* The items to show within this menu. If this ObservableList is modified at
* runtime, the Menu will update as expected.
*/
public final
ObservableList<
MenuItem>
getItems() {
return
items;
}
/**
* If the Menu is not disabled and the {@link ContextMenu} is not already showing,
* then this will cause the {@link ContextMenu} to be shown.
*/
public void
show() {
if (
isDisable()) return;
setShowing(true);
}
/**
* Hides the {@link ContextMenu} if it was previously showing, and any showing
* submenus. If this menu is not showing, then invoking this function
* has no effect.
*/
public void
hide() {
if (!
isShowing()) return;
// hide all sub menus
for (
MenuItem i :
getItems()) {
if (
i instanceof
Menu) {
final
Menu m = (
Menu)
i;
m.
hide();
}
}
setShowing(false);
}
/** {@inheritDoc} */
@
Override public <E extends
Event> void
addEventHandler(
EventType<E>
eventType,
EventHandler<E>
eventHandler) {
eventHandlerManager.
addEventHandler(
eventType,
eventHandler);
}
/** {@inheritDoc} */
@
Override public <E extends
Event> void
removeEventHandler(
EventType<E>
eventType,
EventHandler<E>
eventHandler) {
eventHandlerManager.
removeEventHandler(
eventType,
eventHandler);
}
/** {@inheritDoc} */
@
Override public
EventDispatchChain buildEventDispatchChain(
EventDispatchChain tail) {
return
tail.
prepend(
eventHandlerManager);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final
String DEFAULT_STYLE_CLASS = "menu";
private static final
String STYLE_CLASS_SHOWING = "showing";
}