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