GUIs — Overview
The GUI system is built on Bukkit’s InventoryHolder pattern. Every menu holds its own Inventory, handles its own click events, and is tracked per-player — no global slot maps, no hard-coded switch statements.
Key classes
Section titled “Key classes”| Class | Role |
|---|---|
Menu |
Abstract base for all GUIs — title, size, items, template, click dispatch |
@MenuMeta |
Class annotation declaring a menu’s title / size / permission (like @CommandMeta) |
MenuButton |
A permission-aware slot node: icon + click handler + permission + visibility predicate |
PaginatedMenu |
Extends Menu with automatic page layout and navigation buttons |
MenuItem |
An ItemStack paired with an optional click handler (Consumer<InventoryClickEvent>) |
PlayerMenuContext |
Per-player object holding state, arbitrary data, and navigation history |
MenuManager |
Service — manages PlayerMenuContext instances and closes menus on shutdown |
MenuRegistry |
Service — registers menus by key and opens them by name (like CommandRegistry) |
MenuFactory |
Functional interface that builds a Menu for a context (e.g. ShopMenu::new) |
MenuListener |
Routes InventoryClickEvent/InventoryDragEvent to the correct Menu |
Declarative menus (the command-style API)
Section titled “Declarative menus (the command-style API)”A menu is declared the same way a command is: metadata in an annotation, content declared fluently.
Annotate the class with @MenuMeta (so you no longer override title()/size()), then declare
each slot with the fluent, permission-aware button(int) API inside decorate():
@MenuMeta(title = "<dark_purple>Example Shop", size = 27, permission = "core.shop")public final class ShopMenu extends Menu {
public ShopMenu(CorePlugin plugin, PlayerMenuContext context) { super(plugin, context); }
@Override protected MenuTemplate template() { return Templates.bordered(); }
@Override protected void decorate() { button(13) .icon(new ItemBuilder(Material.DIAMOND).name("<aqua>Buy a diamond").build()) .onClick(event -> context.player().sendMessage(ColorUtils.parse("<green>Bought!"))) .add();
button(15) .icon(new ItemBuilder(Material.CHEST).name("<gold>Restock").build()) .permission("core.shop.admin") // hidden + click-guarded for players without it .onClick(event -> restock()) .add(); }}A button with a permission (or a visibleIf predicate) is auto-hidden during render for
players who fail it, and its click is guarded even if somehow triggered — mirroring how the
command tree hides subcommands in tab-complete and re-checks permission before running. Because a
hidden button leaves its slot empty, put a template filler underneath if your
layout assumes a fixed grid.
@MenuMeta is optional and additive: existing menus that override title()/size() and use
set(slot, MenuItem.of(...)) keep working unchanged.
Creating a menu
Section titled “Creating a menu”public final class MyMenu extends Menu {
public MyMenu(CorePlugin plugin, PlayerMenuContext context) { super(plugin, context); }
@Override public Component title() { return ColorUtils.parse("<dark_purple><bold>My Menu"); }
@Override public int size() { return 27; // must be a multiple of 9 }
@Override protected MenuTemplate template() { return Templates.bordered(); // optional; null = no template }
@Override protected void decorate() { set(13, MenuItem.of( new ItemBuilder(Material.EMERALD) .name("<green>Click me!") .lore("<gray>Does something cool") .build(), event -> context.player().sendMessage( ColorUtils.parse("<green>You clicked!")))); }}Opening a menu
Section titled “Opening a menu”Always open through MenuManager to ensure the context exists:
MenuManager menus = service(MenuManager.class);PlayerMenuContext context = menus.context(player);new MyMenu(plugin, context).open();Or from a command:
@Overridepublic void run(CommandContext context, Arguments args) throws CommandException { new MyMenu(plugin, menus.context(context.player())).open();}MenuItem
Section titled “MenuItem”// Non-interactive display itemMenuItem.display(itemStack)
// Interactive item with click handlerMenuItem.of(itemStack, event -> { /* handle click */ })Clicks are cancelled by default to prevent item theft — except on declared input slots where the player is allowed to place and take items.
Navigation
Section titled “Navigation”PlayerMenuContext maintains a navigation stack:
// From inside a menu or click handler:back() // reopen the previous menu, or close if history is emptyclose() // close the inventoryrefresh() // rebuild the menu in place (useful after state changes)PlayerMenuContext — sharing data between menus
Section titled “PlayerMenuContext — sharing data between menus”// In the first menucontext.set("selectedItem", someItem);
// In the next menuItemStack item = context.get("selectedItem");MenuManager
Section titled “MenuManager”MenuManager creates and caches one PlayerMenuContext per player. When the plugin disables, it calls closeAll() to close every open menu:
MenuManager menus = service(MenuManager.class);menus.context(player); // get or create contextmenus.forget(player); // remove context (call on PlayerQuitEvent)menus.closeAll(); // close all open menusMenuListener
Section titled “MenuListener”MenuListener is registered automatically by MenuModule. It intercepts all InventoryClickEvents, checks if the top inventory’s holder is a Menu, and delegates to menu.handleClick(event). It also routes InventoryCloseEvent to onClose. Drag events in menus are cancelled unless every affected slot is an input slot.
Input slots
Section titled “Input slots”By default a menu is read-only. To let the player place and take an item in a specific slot —
an item-editor GUI, an enchanting/upgrade input, an anvil-style rename — declare it as an input
slot from decorate():
@Overrideprotected void decorate() { setItem(11, MenuItem.display(new ItemBuilder(Material.ARROW).name("<gray>Put an item ->").build())); inputSlot(12); // slot 12 is freely editable by the player}Clicks and drags into an input slot are not cancelled, so the item moves normally. Input slot
declarations are cleared and re-applied on every open()/refresh().
Close handling
Section titled “Close handling”Override onClose(InventoryCloseEvent) to react when the menu is closed — most importantly to
return an item left in an input slot so it is never lost:
private static final int INPUT = 12;
@Overrideprotected void onClose(InventoryCloseEvent event) { ItemStack left = inventory.getItem(INPUT); if (left == null || left.getType().isAir()) { return; } inventory.setItem(INPUT, null); Player player = context.player(); player.getInventory().addItem(left).values() .forEach(overflow -> player.getWorld().dropItemNaturally(player.getLocation(), overflow));}