|
| 1 | +package com.example.exampleplugin.registration |
| 2 | + |
| 3 | +import net.kyori.adventure.text.Component |
| 4 | +import net.kyori.adventure.text.format.NamedTextColor |
| 5 | +import org.bukkit.Material |
| 6 | +import org.bukkit.entity.Player |
| 7 | +import org.bukkit.event.inventory.InventoryClickEvent |
| 8 | +import org.bukkit.event.inventory.InventoryCloseEvent |
| 9 | +import org.bukkit.inventory.Inventory |
| 10 | +import org.bukkit.inventory.ItemStack |
| 11 | +import java.util.UUID |
| 12 | + |
| 13 | +/** |
| 14 | + * A [PluginGUI] subclass that automatically splits content across |
| 15 | + * multiple pages with **Previous** and **Next** navigation buttons. |
| 16 | + * |
| 17 | + * Extend this class and place the subclass anywhere inside the |
| 18 | + * `com.example.exampleplugin.guis` package (or any subpackage) to |
| 19 | + * have it automatically discovered and registered at startup. |
| 20 | + * |
| 21 | + * The bottom row of the inventory is reserved for navigation controls. |
| 22 | + * Content slots are every slot **except** the last row. For example, a |
| 23 | + * 6-row GUI provides 45 content slots per page (rows 1–5). |
| 24 | + * |
| 25 | + * The class must have either: |
| 26 | + * - A no-arg constructor, **or** |
| 27 | + * - A constructor that accepts a single `JavaPlugin` parameter (the plugin |
| 28 | + * instance will be injected automatically). |
| 29 | + * |
| 30 | + * Example: |
| 31 | + * ```kotlin |
| 32 | + * package com.example.exampleplugin.guis |
| 33 | + * |
| 34 | + * import org.bukkit.Material |
| 35 | + * import org.bukkit.inventory.ItemStack |
| 36 | + * |
| 37 | + * class RewardsGUI : PagedPluginGUI( |
| 38 | + * id = "rewards", |
| 39 | + * title = Component.text("Rewards"), |
| 40 | + * rows = 6 |
| 41 | + * ) { |
| 42 | + * override fun getItems(player: Player): List<ItemStack> { |
| 43 | + * return List(100) { index -> |
| 44 | + * val item = ItemStack(Material.DIAMOND) |
| 45 | + * val meta = item.itemMeta |
| 46 | + * meta.displayName(Component.text("Reward #${index + 1}")) |
| 47 | + * item.itemMeta = meta |
| 48 | + * item |
| 49 | + * } |
| 50 | + * } |
| 51 | + * } |
| 52 | + * ``` |
| 53 | + */ |
| 54 | +abstract class PagedPluginGUI( |
| 55 | + id: String, |
| 56 | + title: Component, |
| 57 | + rows: Int = 6 |
| 58 | +) : PluginGUI(id, title, rows) { |
| 59 | + |
| 60 | + /** Tracks the current page for each player viewing this GUI. */ |
| 61 | + private val playerPages = mutableMapOf<UUID, Int>() |
| 62 | + |
| 63 | + /** |
| 64 | + * Returns all items that should be distributed across pages for the |
| 65 | + * given player. The list may be of any size; items are automatically |
| 66 | + * split into pages of [contentSlots] each. |
| 67 | + * |
| 68 | + * @param player the player the GUI is being opened for |
| 69 | + * @return the full list of items to paginate |
| 70 | + */ |
| 71 | + abstract fun getItems(player: Player): List<ItemStack> |
| 72 | + |
| 73 | + /** |
| 74 | + * Called when a player clicks a **content slot** (not a navigation |
| 75 | + * button). Override to add custom click handling. |
| 76 | + * |
| 77 | + * Clicks are cancelled by default to prevent item theft. |
| 78 | + * |
| 79 | + * @param event the inventory click event |
| 80 | + * @param page the page the player is currently viewing (zero-based) |
| 81 | + */ |
| 82 | + open fun onContentClick(event: InventoryClickEvent, page: Int) {} |
| 83 | + |
| 84 | + /** The number of usable content slots per page (all rows except the last). */ |
| 85 | + private val contentSlots: Int |
| 86 | + get() = (rows - 1) * ROW_SIZE |
| 87 | + |
| 88 | + /** The first slot index of the navigation row (the last row). */ |
| 89 | + private val navRowStart: Int |
| 90 | + get() = contentSlots |
| 91 | + |
| 92 | + // ----- PluginGUI overrides ------------------------------------------------ |
| 93 | + |
| 94 | + override fun setup(player: Player, inventory: Inventory) { |
| 95 | + playerPages[player.uniqueId] = 0 |
| 96 | + renderPage(player, inventory, 0) |
| 97 | + } |
| 98 | + |
| 99 | + override fun onClick(event: InventoryClickEvent) { |
| 100 | + event.isCancelled = true |
| 101 | + val player = event.whoClicked as? Player ?: return |
| 102 | + val page = playerPages[player.uniqueId] ?: return |
| 103 | + val slot = event.rawSlot |
| 104 | + |
| 105 | + // Ignore clicks outside the GUI inventory |
| 106 | + if (slot < 0 || slot >= rows * ROW_SIZE) return |
| 107 | + |
| 108 | + when (slot) { |
| 109 | + navRowStart + PREVIOUS_OFFSET -> { |
| 110 | + if (page > 0) openPage(player, event.inventory, page - 1) |
| 111 | + } |
| 112 | + navRowStart + NEXT_OFFSET -> { |
| 113 | + val totalPages = totalPages(getItems(player).size) |
| 114 | + if (page < totalPages - 1) openPage(player, event.inventory, page + 1) |
| 115 | + } |
| 116 | + else -> { |
| 117 | + if (slot < contentSlots) onContentClick(event, page) |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + override fun onClose(event: InventoryCloseEvent) { |
| 123 | + val player = event.player as? Player ?: return |
| 124 | + playerPages.remove(player.uniqueId) |
| 125 | + } |
| 126 | + |
| 127 | + // ----- Internal helpers --------------------------------------------------- |
| 128 | + |
| 129 | + /** |
| 130 | + * Calculates the total number of pages based on the given [itemCount]. |
| 131 | + */ |
| 132 | + private fun totalPages(itemCount: Int): Int { |
| 133 | + if (itemCount == 0) return 1 |
| 134 | + return (itemCount + contentSlots - 1) / contentSlots |
| 135 | + } |
| 136 | + |
| 137 | + /** Switches the player to the given [page] and re-renders the inventory. */ |
| 138 | + private fun openPage(player: Player, inventory: Inventory, page: Int) { |
| 139 | + playerPages[player.uniqueId] = page |
| 140 | + renderPage(player, inventory, page) |
| 141 | + } |
| 142 | + |
| 143 | + /** Clears the inventory and populates it with the items for [page]. */ |
| 144 | + private fun renderPage(player: Player, inventory: Inventory, page: Int) { |
| 145 | + inventory.clear() |
| 146 | + |
| 147 | + val items = getItems(player) |
| 148 | + val totalPages = totalPages(items.size) |
| 149 | + val start = page * contentSlots |
| 150 | + val end = minOf(start + contentSlots, items.size) |
| 151 | + |
| 152 | + for (i in start until end) { |
| 153 | + inventory.setItem(i - start, items[i]) |
| 154 | + } |
| 155 | + |
| 156 | + // Navigation row |
| 157 | + if (page > 0) { |
| 158 | + inventory.setItem(navRowStart + PREVIOUS_OFFSET, createNavItem( |
| 159 | + Material.ARROW, |
| 160 | + Component.text("Previous Page", NamedTextColor.YELLOW) |
| 161 | + )) |
| 162 | + } |
| 163 | + |
| 164 | + inventory.setItem(navRowStart + PAGE_INDICATOR_OFFSET, createNavItem( |
| 165 | + Material.PAPER, |
| 166 | + Component.text("Page ${page + 1}/$totalPages", NamedTextColor.WHITE) |
| 167 | + )) |
| 168 | + |
| 169 | + if (page < totalPages - 1) { |
| 170 | + inventory.setItem(navRowStart + NEXT_OFFSET, createNavItem( |
| 171 | + Material.ARROW, |
| 172 | + Component.text("Next Page", NamedTextColor.YELLOW) |
| 173 | + )) |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + /** Creates a navigation item with the given material and display name. */ |
| 178 | + private fun createNavItem(material: Material, name: Component): ItemStack { |
| 179 | + val item = ItemStack(material) |
| 180 | + val meta = item.itemMeta |
| 181 | + meta.displayName(name) |
| 182 | + item.itemMeta = meta |
| 183 | + return item |
| 184 | + } |
| 185 | + |
| 186 | + companion object { |
| 187 | + private const val ROW_SIZE = 9 |
| 188 | + private const val PREVIOUS_OFFSET = 0 |
| 189 | + private const val PAGE_INDICATOR_OFFSET = 4 |
| 190 | + private const val NEXT_OFFSET = 8 |
| 191 | + } |
| 192 | +} |
0 commit comments