Skip to content

v5 Menus

Jake Moore edited this page Aug 31, 2026 · 7 revisions

Menus

⚠️ Usage ⚠️

Available in spigot-utils and its inheritors (spigot-jar). In v3 this was the "GUI system" in com.kamikazejam.kamicommon.gui. It moved to com.kamikazejam.kamicommon.menu in v4.

A menu belongs to one player, is built through a builder, and is opened with open().

Three kinds

type for
SimpleMenu a fixed set of icons in a fixed size
PaginatedMenu a list of icons spread over pages, with next/previous controls
OneClickMenu allows exactly one click. Close it yourself from the handler if you want that

All three are final. Build them, do not extend them.

SimpleMenu

SimpleMenu menu = SimpleMenu.Builder.create(3)          // 3 rows
        .titleFromMiniMessage("<gold>Shop")
        .modifyIcons(icons -> {
            icons.setMenuIcon("buy", new ItemBuilder(XMaterial.EMERALD), 11);
            icons.setMenuClick("buy", data -> data.getPlayer().sendMessage("bought"));
        })
        .build(player);

menu.open();

Builder.create(...) takes a MenuSize, an int row count, or an InventoryType. The constructors take the same, plus (MenuSize, MenuEvents<M>, MenuOptions<M>) if you want to supply your own. That three-argument form exists only in the MenuSize flavour.

PaginatedMenu

PaginatedMenu menu = new PaginatedMenu.Builder(new SimplePaginationLayout(), 6)
        .titleFromMiniMessage("<gold>Backpack")
        .modifyPageIcons(pages -> {
            for (ItemStack item : contents) {
                pages.addPagedIcon(new ItemBuilder(item));
            }
        })
        .build(player);

menu.open();        // page 0
menu.open(2);       // straight to page 3
menu.getCurrentPage();

⚠️ PaginatedMenu.Builder has no static create(...) factories and the layout is mandatory. This is the first thing that breaks a copy-pasted SimpleMenu example.

The PaginationLayout decides which slots hold paged icons and where the next and previous controls go. SimplePaginationLayout is the default; implement the interface for anything else. Titles come from AbstractPaginatedMenuTitle, with DefaultPaginatedMenuTitle supplied.

open(int) throws IllegalStateException if the layout yields no usable slots for the menu size.

OneClickMenu

OneClickMenu menu = OneClickMenu.Builder.create(1)
        .titleFromMiniMessage("<red>Confirm?")
        .build(player, data -> {
            // the single click
        });

menu.open();

build takes either a MenuClick<OneClickMenu> or a MenuClickTransform<OneClickMenu>. The no-arg open() resets the used-click flag, so the menu is reusable; open(boolean) and reopenMenu(...) do not. oneClickOptions(...) controls whether clicking the filler counts.

The shared builder

Every builder has these, and each returns the builder:

size(MenuSize)
titleFromMiniMessage(String)      titleFromLegacySection(String)
titleFromComponent(VersionedComponent)
title(ComponentMenuTitleProvider)       // per-player titles
titleReplacement(CharSequence, CharSequence)
options(Consumer<MenuOptions<M>>)
events(Consumer<MenuEvents<M>>)
fillerIcon(@Nullable MenuIcon<M>)
modifyIcons(Consumer<IMenuIconsAccess<M>>)

titleReplacement is how you get the player's name into a title that came from config:

.titleFromMiniMessage(config.getString("title"))       // "<gold>{player}'s bag"
.titleReplacement("{player}", player.getName())

The plain title(String) and title(MenuTitleProvider) overloads are deprecated. A bare lambda is ambiguous between title(MenuTitleProvider) and title(ComponentMenuTitleProvider), whose functional interfaces differ only in return type. Cast to (ComponentMenuTitleProvider) if you hit that.

After building

menu.open();                     // returns @Nullable InventoryView, null if the player is not valid
menu.open(true);                 // reset the tick counter, as if freshly opened
menu.close();
menu.reopenMenu();               menu.reopenMenu(boolean reset);
menu.setSize(MenuSize);          menu.resizeMenu(MenuSize);   // both work while the menu is open
menu.modifyIcons(...);           menu.placeIcons(predicate);  // null predicate refreshes everything
menu.getPlayer();  menu.getEvents();  menu.getOptions();  menu.getMenuSize();

Call placeIcons(null) after changing icons from inside a click handler, to make the change visible immediately.

Icons

A MenuIcon<M> wraps one or more ItemBuilders plus a click handler.

MenuIcon<SimpleMenu> icon = new MenuIcon<>(new ItemBuilder(XMaterial.DIAMOND));
icon.setMenuClick(data -> data.getPlayer().sendMessage("clicked"));
icon.setClickSound(XSound.ITEM_GOAT_HORN_SOUND_0);

An icon does not know its own slot. The menu assigns it, either through config or through IMenuIconsAccess.

Give an icon several builders and it cycles between them every builderRotateTicks ticks (default 20).

Modifiers and auto-update

A modifier rebuilds the icon's item on a schedule, which is how you get live data into a menu:

icon.setAutoUpdate(builder -> builder.displayName(
        serializer.fromMiniMessage("<gray>Online: " + Bukkit.getOnlinePlayers().size())), 20);

⚠️ Modifiers must return the ItemBuilder. In v4 they returned void and mutated in place. A modifier that mutates and returns nothing no longer has any effect. Returning a value lets you swap the prototype.

StaticIconModifier receives the builder; StatefulIconModifier also receives the previous ItemStack, the player and the tick.

Clicks

One interface, MenuClick<M>, receives a MenuClickData<M> carrying the player, click type, raw event, slot, the menu itself, and the page for paginated menus.

icons.setMenuClick("buy", data -> {
    Player p = data.getPlayer();
    if (data.getClickType().isRightClick()) { /* ... */ }
});

PlayerSlotClick<M> with PlayerClickData<M> handles clicks in the player's own inventory.

Migrating from v4: MenuClickEvent and MenuClickPage are gone, replaced by MenuClick<M>, and the v4 transform classes were consolidated into MenuClickTransform<M> and PlayerSlotClickTransform. Everything is generic on the menu type now, so MenuIcon becomes MenuIcon<SimpleMenu> and so on.

Events and options

.events(events -> {
    events.addOpenCallback((player, view) -> { });
    events.addCloseCallback((player, event) -> { });
    events.addClickPredicate(event -> true);        // false blocks the click
})
.options(options -> {
    options.getExcludedFillSlots().add(4);
})

Every add* has an overload taking a String id so you can remove it again later.

MenuOptions also carries the drag toggles, and both default to true, so a player cannot drag items anywhere while the menu is open until one of them is turned off:

  • cancelDragEvent cancels drags that touch at least one menu slot
  • cancelPlayerDragEvent cancels drags confined to the player's own inventory

A drag touching any menu slot is judged by cancelDragEvent alone; cancelPlayerDragEvent never applies to it.

Migrating from v4: the MenuEventsModification and MenuOptionsModification interfaces were removed in favour of plain Consumers, so a v4 lambda still works but the type name is gone.

Fillers

Empty slots are filled automatically. To change or disable:

.fillerIcon(null)                                        // no filler
.fillerIcon(new MenuIcon<>(new ItemBuilder(XMaterial.BLACK_STAINED_GLASS_PANE)))

MenuIcon.Config sets the global default filler item.

See also

Clone this wiki locally