The Menu Handler
The Menu Handler
MenuHandler<C> is the optional hook surface for open, close, click, and drag
events. Use it when a menu needs lifecycle logic in addition to rendering.
If you want one type that combines menu layout and lifecycle hooks, implement:
PaperInteractiveMenuon PaperInteractiveMenu<String>on Spigot
A Simple Guarded Menu
public final class ConfirmMenu implements PaperInteractiveMenu {
@Override
public Component title(MenuView<Component, ?> view) {
return Component.text("Confirm");
}
@Override
public Capacity capacity(MenuView<Component, ?> view) {
return Capacity.ofRows(3);
}
@Override
public Content content(MenuView<Component, ?> view) {
return Content.builder(view.capacity())
.set(1, 4, Button.clickable(
new ItemStack(Material.EMERALD_BLOCK),
(menuView, event) ->
menuView.viewer().sendMessage(Component.text("Confirmed"))
))
.build();
}
@Override
public boolean onPreClick(MenuView<Component, ?> view, InventoryClickEvent event) {
Slot confirmSlot = Slot.at(1, 4, view.capacity());
if (!Slot.of(event.getSlot()).equals(confirmSlot)) {
view.viewer().sendMessage(Component.text("Click the center button."));
return false;
}
return true;
}
@Override
public void onOpen(MenuView<Component, ?> view, InventoryOpenEvent event) {
view.viewer().sendMessage(Component.text("Menu opened"));
}
@Override
public void onClose(MenuView<Component, ?> view, InventoryCloseEvent event) {
view.viewer().sendMessage(Component.text("Menu closed"));
}
}
}</CodeTabItem> <CodeTabItem value="spigot" label="Spigot 1.8.8" language="java">{
public final class ConfirmMenu implements InteractiveMenu
@Override
public String title(MenuView<String, ?> view) {
return ChatColor.GOLD + "Confirm";
}
@Override
public Capacity capacity(MenuView<String, ?> view) {
return Capacity.ofRows(3);
}
@Override
public Content content(MenuView<String, ?> view) {
return Content.builder(view.capacity())
.set(1, 4, Button.clickable(
new ItemStack(Material.EMERALD_BLOCK),
(menuView, event) ->
menuView.viewer().sendMessage(ChatColor.GREEN + "Confirmed")
))
.build();
}
@Override
public boolean onPreClick(MenuView<String, ?> view, InventoryClickEvent event) {
Slot confirmSlot = Slot.at(1, 4, view.capacity());
if (!Slot.of(event.getSlot()).equals(confirmSlot)) {
view.viewer().sendMessage(ChatColor.RED + "Click the center button.");
return false;
}
return true;
}
@Override
public void onOpen(MenuView<String, ?> view, InventoryOpenEvent event) {
view.viewer().sendMessage(ChatColor.YELLOW + "Menu opened");
}
@Override
public void onClose(MenuView<String, ?> view, InventoryCloseEvent event) {
view.viewer().sendMessage(ChatColor.YELLOW + "Menu closed");
}
} `}
The Real Click Flow
For clicks in the top menu inventory, Lotus behaves like this:
Lotus listener cancels the top-inventory click
-> onPreClick(view, event)
-> if true: button.dispatch(...)
-> Lotus repaints the inventory
-> onPostClick(view, event)
If onPreClick(...) returns false, Lotus stops there.
onPostClick(...) runs only when onPreClick(...) returned true.
What Lotus Already Does For You
- top-inventory clicks are cancelled before dispatch
- drag events over the menu are cancelled
- the clicked button is looked up and dispatched
- the inventory is repainted after dispatch
That means MenuHandler is for behavior, not for basic protection.
One Important Limitation
Clicks in the player inventory below the menu do not go through MenuHandler.
Those are controlled by Lotus.Options.allowBottomInventoryClick(...).
If your design depends on completely freezing the player's own inventory while the menu is open,
set allowBottomInventoryClick(false) when you create the Lotus runtime.
When To Use Each Hook
| Hook | Best use |
|---|---|
onPreClick |
Gate or deny a click before button logic runs |
onPostClick |
Follow-up logic after a successful dispatch |
onOpen |
Initialize view-local state or notify the player |
onClose |
Cleanup or commit data |
onDrag |
React to drag attempts over the menu |
Next: Button Variants.