From 73b2014afd875546ce57c07662d8c6509f72ad74 Mon Sep 17 00:00:00 2001 From: Touchie771 Date: Sun, 23 Nov 2025 17:42:25 +0200 Subject: [PATCH] implemented click handler logic --- .../minecraftGUI/api/ClickHandler.java | 154 ++++++++++++++ .../me/touchie771/minecraftGUI/api/Menu.java | 134 +++++++++++- .../minecraftGUI/api/MenuClickHandler.java | 199 ++++++++++++++++++ 3 files changed, 476 insertions(+), 11 deletions(-) create mode 100644 src/main/java/me/touchie771/minecraftGUI/api/ClickHandler.java create mode 100644 src/main/java/me/touchie771/minecraftGUI/api/MenuClickHandler.java diff --git a/src/main/java/me/touchie771/minecraftGUI/api/ClickHandler.java b/src/main/java/me/touchie771/minecraftGUI/api/ClickHandler.java new file mode 100644 index 0000000..6e6d8d0 --- /dev/null +++ b/src/main/java/me/touchie771/minecraftGUI/api/ClickHandler.java @@ -0,0 +1,154 @@ +package me.touchie771.minecraftGUI.api; + +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.function.Consumer; + +/** + * Handles click events for menu slots with filtering and cancellation options. + * + *

This class provides a way to register callbacks for inventory clicks with + * fine-grained control over which click types are handled and whether the event + * should be automatically cancelled.

+ * + *

Example usage: + *

{@code
+ * ClickHandler handler = ClickHandler.newBuilder()
+ *     .callback(event -> {
+ *         Player player = (Player) event.getWhoClicked();
+ *         player.sendMessage("You clicked the item!");
+ *     })
+ *     .filter(ClickType.LEFT, ClickType.RIGHT)
+ *     .autoCancel(true)
+ *     .build();
+ * }

+ */ +public class ClickHandler { + + private final Consumer callback; + private final EnumSet allowedClickTypes; + private final boolean autoCancel; + + /** + * Creates a new ClickHandler with the specified configuration. + * This constructor is package-private and should only be called by ClickHandlerBuilder. + * + * @param builder the configured builder containing handler properties + * @throws IllegalArgumentException if builder or callback is null + */ + ClickHandler(@NotNull ClickHandlerBuilder builder) { + if (builder.callback == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + this.callback = builder.callback; + this.allowedClickTypes = builder.allowedClickTypes != null ? + EnumSet.copyOf(builder.allowedClickTypes) : EnumSet.allOf(ClickType.class); + this.autoCancel = builder.autoCancel; + } + + /** + * Handles the given inventory click event if it matches the configured filters. + * + * @param event the inventory click event to handle + */ + public void handleClick(@NotNull InventoryClickEvent event) { + + // Check if the click type is allowed + if (!allowedClickTypes.contains(event.getClick())) { + return; + } + + // Auto-cancel if configured + if (autoCancel) { + event.setCancelled(true); + } + + // Execute the callback + try { + callback.accept(event); + } catch (Exception e) { + // Log the exception but don't rethrow to avoid breaking other handlers + System.err.println("Error in ClickHandler callback: " + e.getMessage()); + } + + } + + /** + * Creates a new ClickHandlerBuilder instance for constructing click handlers. + * This is the recommended way to create new ClickHandler instances. + * + * @return a new ClickHandlerBuilder ready for configuration + */ + @Contract(" -> new") + public static @NotNull ClickHandlerBuilder newBuilder() { + return new ClickHandlerBuilder(); + } + + /** + * Builder class for creating ClickHandler instances with a fluent API. + * + *

This builder allows you to configure all aspects of a click handler before creation: + * the callback function, allowed click types, and auto-cancellation behavior.

+ */ + public static class ClickHandlerBuilder { + + private Consumer callback; + private EnumSet allowedClickTypes; + private boolean autoCancel = true; + + /** + * Sets the callback function to execute when a click event is handled. + * + * @param callback the consumer that will receive the click event + * @return this builder instance for method chaining + * @throws IllegalArgumentException if callback is null + */ + public ClickHandlerBuilder callback(@NotNull Consumer callback) { + this.callback = callback; + return this; + } + + /** + * Sets which click types should be handled. + * If not specified, all click types will be handled. + * + * @param clickTypes the click types to handle + * @return this builder instance for method chaining + */ + public ClickHandlerBuilder filter(@NotNull ClickType... clickTypes) { + if (clickTypes == null || clickTypes.length == 0) { + this.allowedClickTypes = EnumSet.allOf(ClickType.class); + } else { + this.allowedClickTypes = EnumSet.copyOf(Arrays.asList(clickTypes)); + } + return this; + } + + /** + * Sets whether the event should be automatically cancelled before executing the callback. + * Default is true. + * + * @param autoCancel true to auto-cancel events, false otherwise + * @return this builder instance for method chaining + */ + public ClickHandlerBuilder autoCancel(boolean autoCancel) { + this.autoCancel = autoCancel; + return this; + } + + /** + * Builds and returns a new ClickHandler instance with the configured properties. + * + * @return a new ClickHandler instance + * @throws IllegalArgumentException if callback is not set + */ + public ClickHandler build() { + return new ClickHandler(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/touchie771/minecraftGUI/api/Menu.java b/src/main/java/me/touchie771/minecraftGUI/api/Menu.java index bc2df89..7280715 100644 --- a/src/main/java/me/touchie771/minecraftGUI/api/Menu.java +++ b/src/main/java/me/touchie771/minecraftGUI/api/Menu.java @@ -2,14 +2,20 @@ import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; /** * Represents a Minecraft GUI menu that can be displayed to players. @@ -32,6 +38,7 @@ public class Menu { private final Inventory menu; private final HashSet items; + private final MenuClickHandler clickHandler; /** * Creates a new menu from the specified builder. @@ -40,15 +47,29 @@ public class Menu { * @param builder the configured builder containing menu properties * @throws IllegalArgumentException if builder is null */ - public Menu(@NotNull MenuBuilder builder) { + public Menu(@NotNull MenuBuilder builder, @NotNull Plugin plugin) { this.menu = Bukkit.createInventory(null, builder.inventorySize, builder.inventoryTitle); this.items = new HashSet<>(builder.items); + this.clickHandler = new MenuClickHandler(this.menu, plugin.getLogger()); + + // Copy click handlers from builder + for (Map.Entry entry : builder.clickHandlers.entrySet()) { + this.clickHandler.setClickHandler(entry.getKey(), entry.getValue()); + } + + // Set close handler from builder + if (builder.closeHandler != null) { + this.clickHandler.setCloseHandler(builder.closeHandler); + } for (SlotItem item : items) { ItemStack itemStack = new ItemStack(item.material(), item.quantity()); itemStack.editMeta(meta -> meta.displayName(item.itemName())); menu.setItem(item.itemSlot(), itemStack); } + + // Register event handlers + registerEvents(plugin); } /** @@ -126,17 +147,69 @@ public HashSet getItems() { } /** - * Returns the SlotItem at the specified slot position. - * This is useful for handling inventory click events. + * Registers a click handler for the specified slot. * - * @param slot the slot position to check (0-53 for chest inventories) - * @return the SlotItem at the specified slot, or null if the slot is empty + * @param slot the slot position (0-53 for chest inventories) + * @param handler the click handler to register + * @throws IllegalArgumentException if slot is invalid or handler is null + */ + public void onClick(int slot, @NotNull ClickHandler handler) { + if (slot < 0 || slot > 53) { + throw new IllegalArgumentException("Slot must be between 0 and 53"); + } + clickHandler.setClickHandler(slot, handler); + } + + /** + * Removes the click handler for the specified slot. + * + * @param slot the slot position to remove the handler from + */ + public void removeClickHandler(int slot) { + clickHandler.removeClickHandler(slot); + } + + /** + * Gets the click handler for the specified slot. + * + * @param slot the slot position + * @return the ClickHandler for the slot, or null if none is registered + */ + public @Nullable ClickHandler getClickHandler(int slot) { + return clickHandler.getClickHandler(slot); + } + + /** + * Sets a handler that will be called when the inventory is closed. + * + * @param closeHandler the consumer that will receive the close event + */ + public void onClose(@NotNull Consumer closeHandler) { + clickHandler.setCloseHandler(closeHandler); + } + + /** + * Removes the close handler. + */ + public void removeCloseHandler() { + clickHandler.removeCloseHandler(); + } + + /** + * Registers this menu's event handlers with Bukkit. + * This method is called automatically in the constructor. + */ + private void registerEvents(Plugin plugin) { + Bukkit.getPluginManager().registerEvents(clickHandler, plugin); + } + + /** + * Unregisters this menu's event handlers. + * This method is automatically called when the inventory is closed to prevent memory leaks. + * You only need to call this manually if you want to unregister events before closing the menu. */ - public SlotItem getItemAt(int slot) { - return this.items.stream() - .filter(item -> item.itemSlot() == slot) - .findFirst() - .orElse(null); + public void unregisterEvents() { + clickHandler.unregister(); } /** @@ -171,6 +244,14 @@ public static class MenuBuilder { private int inventorySize; private Component inventoryTitle; private final HashSet items = new HashSet<>(); + private final Map clickHandlers = new HashMap<>(); + private Consumer closeHandler; + private Plugin plugin; + + public MenuBuilder plugin(@NotNull Plugin plugin) { + this.plugin = plugin; + return this; + } /** * Sets the size of the inventory. @@ -244,7 +325,38 @@ public Menu build() { if (inventoryTitle == null) { throw new IllegalArgumentException("Inventory title cannot be null"); } - return new Menu(this); + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null"); + } + return new Menu(this, plugin); + } + + /** + * Registers a click handler for the specified slot. + * + * @param slot the slot position (0-53 for chest inventories) + * @param handler the click handler to register + * @return this builder instance for method chaining + * @throws IllegalArgumentException if slot is invalid or handler is null + */ + public MenuBuilder onClick(int slot, @NotNull ClickHandler handler) { + if (slot < 0 || slot > 53) { + throw new IllegalArgumentException("Slot must be between 0 and 53"); + } + clickHandlers.put(slot, handler); + return this; + } + + /** + * Sets a handler that will be called when the inventory is closed. + * + * @param closeHandler the consumer that will receive the close event + * @return this builder instance for method chaining + * @throws IllegalArgumentException if closeHandler is null + */ + public MenuBuilder onClose(@NotNull Consumer closeHandler) { + this.closeHandler = closeHandler; + return this; } } } \ No newline at end of file diff --git a/src/main/java/me/touchie771/minecraftGUI/api/MenuClickHandler.java b/src/main/java/me/touchie771/minecraftGUI/api/MenuClickHandler.java new file mode 100644 index 0000000..59cfe4e --- /dev/null +++ b/src/main/java/me/touchie771/minecraftGUI/api/MenuClickHandler.java @@ -0,0 +1,199 @@ +package me.touchie771.minecraftGUI.api; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Manages click event handling for a specific menu inventory. + * + *

This class handles the registration and delegation of inventory click events + * for menu slots. It maintains a mapping of slot positions to ClickHandler instances + * and automatically registers with Bukkit's event system.

+ * + *

Each Menu instance has its own MenuClickHandler to ensure proper isolation + * between different menus and prevent cross-contamination of click events.

+ */ +class MenuClickHandler implements Listener { + + private final Inventory menuInventory; + private final Map slotHandlers; + private Consumer closeHandler; + private boolean isRegistered = false; + private final Logger logger; + + /** + * Creates a new MenuClickHandler for the specified inventory. + * + * @param menuInventory the inventory this handler manages + * @throws IllegalArgumentException if menuInventory is null + */ + MenuClickHandler(@NotNull Inventory menuInventory, @NotNull Logger logger) { + this.menuInventory = menuInventory; + this.slotHandlers = new HashMap<>(); + this.logger = logger; + } + + /** + * Registers a click handler for the specified slot. + * + * @param slot the slot position (0-53 for chest inventories) + * @param handler the click handler to register + * @throws IllegalArgumentException if slot is invalid or handler is null + */ + public void setClickHandler(int slot, @NotNull ClickHandler handler) { + if (slot < 0 || slot >= menuInventory.getSize()) { + throw new IllegalArgumentException("Slot " + slot + " is out of bounds for inventory size " + menuInventory.getSize()); + } + slotHandlers.put(slot, handler); + } + + /** + * Removes the click handler for the specified slot. + * + * @param slot the slot position to remove the handler from + */ + public void removeClickHandler(int slot) { + slotHandlers.remove(slot); + } + + /** + * Gets the click handler for the specified slot. + * + * @param slot the slot position + * @return the ClickHandler for the slot, or null if none is registered + */ + public ClickHandler getClickHandler(int slot) { + return slotHandlers.get(slot); + } + + /** + * Sets a handler that will be called when the inventory is closed. + * + * @param closeHandler the consumer that will receive the close event + */ + public void setCloseHandler(@NotNull Consumer closeHandler) { + this.closeHandler = closeHandler; + } + + /** + * Removes the close handler. + */ + public void removeCloseHandler() { + this.closeHandler = null; + } + + /** + * Registers this handler with Bukkit's event system. + * This method should be called when the menu is created or first opened. + */ + public void register() { + if (!isRegistered) { + // Note: In a real implementation, you'd get the plugin instance + // For now, we'll assume this is handled by the Menu class + // Bukkit.getPluginManager().registerEvents(this, plugin); + isRegistered = true; + } + } + + /** + * Unregisters this handler from Bukkit's event system. + * This method should be called when the menu is no longer needed. + */ + public void unregister() { + if (isRegistered) { + HandlerList.unregisterAll(this); + isRegistered = false; + } + } + + /** + * Checks if this handler is currently registered with Bukkit. + * + * @return true if registered, false otherwise + */ + public boolean isRegistered() { + return isRegistered; + } + + /** + * Handles inventory click events and delegates them to the appropriate slot handler. + * + * @param event the inventory click event + */ + @EventHandler + public void onInventoryClick(@NotNull InventoryClickEvent event) { + // Only handle clicks for our specific inventory + if (!event.getInventory().equals(menuInventory)) { + return; + } + + // Get the clicked slot (use raw slot to get actual position in the open inventory) + int slot = event.getRawSlot(); + + // Ignore clicks outside the inventory + if (slot < 0 || slot >= menuInventory.getSize()) { + return; + } + + // Check if we have a handler for this slot + ClickHandler handler = slotHandlers.get(slot); + if (handler != null) { + handler.handleClick(event); + } + } + + /** + * Handles inventory close events and calls the registered close handler if present. + * + * @param event the inventory close event + */ + @EventHandler + public void onInventoryClose(@NotNull InventoryCloseEvent event) { + // Only handle closes for our specific inventory + if (!event.getInventory().equals(menuInventory)) { + return; + } + + // Call the close handler if present + if (closeHandler != null) { + try { + closeHandler.accept(event); + } catch (Exception e) { + // Log the exception but don't rethrow + logger.log(Level.SEVERE, "Error in close handler", e); + } + } + + // Auto-unregister to prevent memory leaks + unregister(); + } + + /** + * Clears all click handlers and the close handler. + * This is useful when resetting a menu or preparing it for garbage collection. + */ + public void clear() { + slotHandlers.clear(); + closeHandler = null; + } + + /** + * Gets the number of registered click handlers. + * + * @return the number of slots with click handlers + */ + public int getHandlerCount() { + return slotHandlers.size(); + } +} \ No newline at end of file