From 29a22cee262f8eac29debf2bb6d73a0270580a85 Mon Sep 17 00:00:00 2001
From: Touchie771
Date: Sat, 29 Nov 2025 19:33:09 +0200
Subject: [PATCH 1/3] added builder pattern to slot item
---
.../touchie771/minecraftGUI/api/SlotItem.java | 136 ++++++++++++++++++
1 file changed, 136 insertions(+)
diff --git a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
index 6e16a57..84cd5f9 100644
--- a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
+++ b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
@@ -46,4 +46,140 @@ public record SlotItem(
public SlotItem(@NotNull Component itemName, int itemSlot, @NotNull Material material, int quantity) {
this(itemName, itemSlot, material, quantity, null, null, null, null, null);
}
+
+ /**
+ * Creates a new builder for constructing SlotItem instances.
+ *
+ * @param itemSlot The slot position where this item should be placed
+ * @return A new Builder instance
+ */
+ public static Builder builder(int itemSlot) {
+ return new Builder(itemSlot);
+ }
+
+ /**
+ * Builder class for creating SlotItem instances with a fluent API.
+ */
+ public static class Builder {
+ private @Nullable Component itemName;
+ private final int itemSlot;
+ private @Nullable Material material;
+ private int quantity = 1;
+ private @Nullable List lore;
+ private @Nullable Map enchantments;
+ private @Nullable ItemStack customItemStack;
+ private @Nullable Integer customModelData;
+ private @Nullable Integer damage;
+
+ private Builder(int itemSlot) {
+ this.itemSlot = itemSlot;
+ }
+
+ /**
+ * Sets the display name of the item.
+ *
+ * @param itemName The display name component
+ * @return This builder instance for chaining
+ */
+ public Builder itemName(@Nullable Component itemName) {
+ this.itemName = itemName;
+ return this;
+ }
+
+ /**
+ * Sets the material type for this item.
+ *
+ * @param material The material type
+ * @return This builder instance for chaining
+ */
+ public Builder material(@Nullable Material material) {
+ this.material = material;
+ return this;
+ }
+
+ /**
+ * Sets the stack size of this item.
+ *
+ * @param quantity The stack size (1-64)
+ * @return This builder instance for chaining
+ */
+ public Builder quantity(int quantity) {
+ this.quantity = quantity;
+ return this;
+ }
+
+ /**
+ * Sets the lore lines for the item.
+ *
+ * @param lore The list of lore components
+ * @return This builder instance for chaining
+ */
+ public Builder lore(@Nullable List lore) {
+ this.lore = lore;
+ return this;
+ }
+
+ /**
+ * Sets the enchantments to apply to the item.
+ *
+ * @param enchantments The map of enchantments to levels
+ * @return This builder instance for chaining
+ */
+ public Builder enchantments(@Nullable Map enchantments) {
+ this.enchantments = enchantments;
+ return this;
+ }
+
+ /**
+ * Sets a custom ItemStack to use as base (e.g., for items with NBT).
+ *
+ * @param customItemStack The custom ItemStack
+ * @return This builder instance for chaining
+ */
+ public Builder customItemStack(@Nullable ItemStack customItemStack) {
+ this.customItemStack = customItemStack;
+ return this;
+ }
+
+ /**
+ * Sets the custom model data ID.
+ *
+ * @param customModelData The custom model data ID
+ * @return This builder instance for chaining
+ */
+ public Builder customModelData(@Nullable Integer customModelData) {
+ this.customModelData = customModelData;
+ return this;
+ }
+
+ /**
+ * Sets the damage/durability value.
+ *
+ * @param damage The damage value
+ * @return This builder instance for chaining
+ */
+ public Builder damage(@Nullable Integer damage) {
+ this.damage = damage;
+ return this;
+ }
+
+ /**
+ * Builds the SlotItem instance with the configured values.
+ *
+ * @return A new SlotItem instance
+ */
+ public SlotItem build() {
+ return new SlotItem(
+ itemName,
+ itemSlot,
+ material,
+ quantity,
+ lore,
+ enchantments,
+ customItemStack,
+ customModelData,
+ damage
+ );
+ }
+ }
}
\ No newline at end of file
From da3dee2746b5ef271abb96fc37fbe29c4bb9bd54 Mon Sep 17 00:00:00 2001
From: Touchie771
Date: Sat, 29 Nov 2025 19:38:52 +0200
Subject: [PATCH 2/3] implemented lore support
---
.../me/touchie771/minecraftGUI/api/Menu.java | 10 +++++--
.../touchie771/minecraftGUI/api/SlotItem.java | 30 +++++++++++++++++++
.../api/presets/ConfirmationMenu.java | 20 +++++--------
.../api/presets/PaginationMenu.java | 26 ++++++++--------
4 files changed, 59 insertions(+), 27 deletions(-)
diff --git a/src/main/java/me/touchie771/minecraftGUI/api/Menu.java b/src/main/java/me/touchie771/minecraftGUI/api/Menu.java
index 447c345..b685b36 100644
--- a/src/main/java/me/touchie771/minecraftGUI/api/Menu.java
+++ b/src/main/java/me/touchie771/minecraftGUI/api/Menu.java
@@ -27,7 +27,10 @@
* Menu menu = Menu.newBuilder()
* .size(27)
* .title(Component.text("My Menu"))
- * .items(new SlotItem(Component.text("Diamond"), (short) 0, Material.DIAMOND, 1))
+ * .items(SlotItem.builder(0)
+ * .itemName(Component.text("Diamond"))
+ * .material(Material.DIAMOND)
+ * .build())
* .build();
* }
*/
@@ -276,7 +279,10 @@ public void unregisterEvents() {
* Menu menu = new MenuBuilder()
* .size(27)
* .title(Component.text("My Menu"))
- * .items(new SlotItem(Component.text("Diamond"), (short) 0, Material.DIAMOND, 1))
+ * .items(SlotItem.builder(0)
+ * .itemName(Component.text("Diamond"))
+ * .material(Material.DIAMOND)
+ * .build())
* .build();
* }
*/
diff --git a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
index 84cd5f9..b8eb8d4 100644
--- a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
+++ b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
@@ -7,6 +7,8 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -119,6 +121,34 @@ public Builder lore(@Nullable List lore) {
return this;
}
+ /**
+ * Adds a single line of lore to the item.
+ *
+ * @param line The lore line to add
+ * @return This builder instance for chaining
+ */
+ public Builder addLore(@NotNull Component line) {
+ if (this.lore == null) {
+ this.lore = new ArrayList<>();
+ }
+ this.lore.add(line);
+ return this;
+ }
+
+ /**
+ * Adds multiple lines of lore to the item.
+ *
+ * @param lines The lore lines to add
+ * @return This builder instance for chaining
+ */
+ public Builder addLore(@NotNull Component... lines) {
+ if (this.lore == null) {
+ this.lore = new ArrayList<>();
+ }
+ this.lore.addAll(Arrays.asList(lines));
+ return this;
+ }
+
/**
* Sets the enchantments to apply to the item.
*
diff --git a/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java b/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java
index be7cc61..f62f261 100644
--- a/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java
+++ b/src/main/java/me/touchie771/minecraftGUI/api/presets/ConfirmationMenu.java
@@ -29,19 +29,15 @@ public class ConfirmationMenu {
* @return A configured Menu instance
*/
public static Menu create(@NotNull Plugin plugin, @NotNull Component title, @NotNull Runnable onConfirm, @NotNull Runnable onCancel) {
- SlotItem confirmItem = new SlotItem(
- Component.text("Confirm", NamedTextColor.GREEN),
- (short) CONFIRM_SLOT,
- Material.LIME_WOOL,
- 1
- );
+ SlotItem confirmItem = SlotItem.builder(CONFIRM_SLOT)
+ .itemName(Component.text("Confirm", NamedTextColor.GREEN))
+ .material(Material.LIME_WOOL)
+ .build();
- SlotItem cancelItem = new SlotItem(
- Component.text("Cancel", NamedTextColor.RED),
- (short) CANCEL_SLOT,
- Material.RED_WOOL,
- 1
- );
+ SlotItem cancelItem = SlotItem.builder(CANCEL_SLOT)
+ .itemName(Component.text("Cancel", NamedTextColor.RED))
+ .material(Material.RED_WOOL)
+ .build();
ClickHandler confirmHandler = ClickHandler.newBuilder()
.callback(event -> {
diff --git a/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java b/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java
index ec3f9eb..a37574a 100644
--- a/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java
+++ b/src/main/java/me/touchie771/minecraftGUI/api/presets/PaginationMenu.java
@@ -79,7 +79,11 @@ private void update() {
PageItem item = items.get(i);
int slot = i - startIndex;
- menu.addItems(new SlotItem(item.name(), (short) slot, item.material(), item.quantity()));
+ menu.addItems(SlotItem.builder(slot)
+ .itemName(item.name())
+ .material(item.material())
+ .quantity(item.quantity())
+ .build());
if (item.action() != null) {
menu.onClick(slot, ClickHandler.newBuilder()
@@ -90,12 +94,10 @@ private void update() {
}
if (currentPage > 0) {
- menu.addItems(new SlotItem(
- Component.text("Previous Page", NamedTextColor.YELLOW),
- (short) PREV_SLOT,
- Material.ARROW,
- 1
- ));
+ menu.addItems(SlotItem.builder(PREV_SLOT)
+ .itemName(Component.text("Previous Page", NamedTextColor.YELLOW))
+ .material(Material.ARROW)
+ .build());
menu.onClick(PREV_SLOT, ClickHandler.newBuilder()
.callback(e -> {
currentPage--;
@@ -106,12 +108,10 @@ private void update() {
}
if (currentPage < totalPages - 1) {
- menu.addItems(new SlotItem(
- Component.text("Next Page", NamedTextColor.YELLOW),
- (short) NEXT_SLOT,
- Material.ARROW,
- 1
- ));
+ menu.addItems(SlotItem.builder(NEXT_SLOT)
+ .itemName(Component.text("Next Page", NamedTextColor.YELLOW))
+ .material(Material.ARROW)
+ .build());
menu.onClick(NEXT_SLOT, ClickHandler.newBuilder()
.callback(e -> {
currentPage++;
From e38e5bf7e91b9e57baf7ac2a936cb555c75ee741 Mon Sep 17 00:00:00 2001
From: Touchie771
Date: Sat, 29 Nov 2025 19:47:24 +0200
Subject: [PATCH 3/3] wrote tests for new slot item
---
.../touchie771/minecraftGUI/api/SlotItem.java | 8 +-
.../minecraftGUI/api/SlotItemTest.java | 283 +++++++++++++++---
2 files changed, 249 insertions(+), 42 deletions(-)
diff --git a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
index b8eb8d4..087cdb8 100644
--- a/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
+++ b/src/main/java/me/touchie771/minecraftGUI/api/SlotItem.java
@@ -130,6 +130,8 @@ public Builder lore(@Nullable List lore) {
public Builder addLore(@NotNull Component line) {
if (this.lore == null) {
this.lore = new ArrayList<>();
+ } else if (!(this.lore instanceof ArrayList)) {
+ this.lore = new ArrayList<>(this.lore);
}
this.lore.add(line);
return this;
@@ -144,6 +146,8 @@ public Builder addLore(@NotNull Component line) {
public Builder addLore(@NotNull Component... lines) {
if (this.lore == null) {
this.lore = new ArrayList<>();
+ } else if (!(this.lore instanceof ArrayList)) {
+ this.lore = new ArrayList<>(this.lore);
}
this.lore.addAll(Arrays.asList(lines));
return this;
@@ -204,8 +208,8 @@ public SlotItem build() {
itemSlot,
material,
quantity,
- lore,
- enchantments,
+ lore != null ? List.copyOf(lore) : null,
+ enchantments != null ? Map.copyOf(enchantments) : null,
customItemStack,
customModelData,
damage
diff --git a/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java b/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java
index 82a545b..ea90715 100644
--- a/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java
+++ b/src/test/java/me/touchie771/minecraftGUI/api/SlotItemTest.java
@@ -1,10 +1,12 @@
package me.touchie771.minecraftGUI.api;
import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
import java.util.List;
import java.util.Map;
@@ -15,64 +17,265 @@
class SlotItemTest {
@Test
- void testLegacyConstructor() {
- Component name = Component.text("Test Item");
- SlotItem item = new SlotItem(name, (short) 0, Material.DIAMOND, 1);
+ @DisplayName("Test builder with minimal configuration")
+ void testBuilderMinimalConfiguration() {
+ int slot = 0;
+ SlotItem item = SlotItem.builder(slot)
+ .material(Material.DIAMOND)
+ .build();
- assertEquals(name, item.itemName());
- assertEquals(0, item.itemSlot());
+ assertNull(item.itemName());
+ assertEquals(slot, item.itemSlot());
assertEquals(Material.DIAMOND, item.material());
- assertEquals(1, item.quantity());
+ assertEquals(1, item.quantity()); // Default quantity
assertNull(item.lore());
assertNull(item.enchantments());
assertNull(item.customItemStack());
+ assertNull(item.customModelData());
+ assertNull(item.damage());
}
@Test
- void testFullConstructor() {
- Component name = Component.text("Full Item");
- List lore = List.of(Component.text("Line 1"));
- Map enchants = Map.of(Enchantment.UNBREAKING, 1);
-
- SlotItem item = new SlotItem(
- name,
- (short) 1,
- Material.GOLD_INGOT,
- 5,
- lore,
- enchants,
- null,
- 123,
- 10
+ @DisplayName("Test builder with full configuration")
+ void testBuilderFullConfiguration() {
+ Component name = Component.text("Full Item", NamedTextColor.GOLD);
+ List lore = List.of(
+ Component.text("First line"),
+ Component.text("Second line", NamedTextColor.RED)
);
+ Map enchants = Map.of(
+ Enchantment.UNBREAKING, 3,
+ Enchantment.SHARPNESS, 5
+ );
+ ItemStack customStack = mock(ItemStack.class);
+
+ SlotItem item = SlotItem.builder(10)
+ .itemName(name)
+ .material(Material.NETHERITE_SWORD)
+ .quantity(1)
+ .lore(lore)
+ .enchantments(enchants)
+ .customItemStack(customStack)
+ .customModelData(1234)
+ .damage(50)
+ .build();
assertEquals(name, item.itemName());
- assertEquals(1, item.itemSlot());
- assertEquals(Material.GOLD_INGOT, item.material());
- assertEquals(5, item.quantity());
+ assertEquals(10, item.itemSlot());
+ assertEquals(Material.NETHERITE_SWORD, item.material());
+ assertEquals(1, item.quantity());
assertEquals(lore, item.lore());
assertEquals(enchants, item.enchantments());
- assertEquals(123, item.customModelData());
- assertEquals(10, item.damage());
+ assertEquals(customStack, item.customItemStack());
+ assertEquals(1234, item.customModelData());
+ assertEquals(50, item.damage());
}
@Test
- void testCustomItemStack() {
- ItemStack stack = mock(ItemStack.class);
- SlotItem item = new SlotItem(
- null,
- (short) 2,
- null,
- 0,
- null,
- null,
- stack,
- null,
- null
- );
+ @DisplayName("Test builder with custom ItemStack only")
+ void testBuilderCustomItemStackOnly() {
+ ItemStack customStack = mock(ItemStack.class);
+
+ SlotItem item = SlotItem.builder(5)
+ .customItemStack(customStack)
+ .quantity(64)
+ .build();
- assertEquals(stack, item.customItemStack());
+ assertNull(item.itemName());
+ assertEquals(5, item.itemSlot());
assertNull(item.material());
+ assertEquals(64, item.quantity());
+ assertNull(item.lore());
+ assertNull(item.enchantments());
+ assertEquals(customStack, item.customItemStack());
+ assertNull(item.customModelData());
+ assertNull(item.damage());
+ }
+
+ @Test
+ @DisplayName("Test addLore with single line")
+ void testAddLoreSingleLine() {
+ Component line = Component.text("A single lore line");
+
+ SlotItem item = SlotItem.builder(0)
+ .material(Material.STONE)
+ .addLore(line)
+ .build();
+
+ assertNotNull(item.lore());
+ assertEquals(1, item.lore().size());
+ assertEquals(line, item.lore().getFirst());
+ }
+
+ @Test
+ @DisplayName("Test addLore with multiple lines")
+ void testAddLoreMultipleLines() {
+ Component line1 = Component.text("First line");
+ Component line2 = Component.text("Second line");
+ Component line3 = Component.text("Third line");
+
+ SlotItem item = SlotItem.builder(0)
+ .material(Material.STONE)
+ .addLore(line1, line2, line3)
+ .build();
+
+ assertNotNull(item.lore());
+ assertEquals(3, item.lore().size());
+ assertEquals(line1, item.lore().get(0));
+ assertEquals(line2, item.lore().get(1));
+ assertEquals(line3, item.lore().get(2));
+ }
+
+ @Test
+ @DisplayName("Test addLore chaining with existing lore")
+ void testAddLoreChainingWithExistingLore() {
+ Component initialLore = Component.text("Initial lore");
+ Component addedLine1 = Component.text("Added line 1");
+ Component addedLine2 = Component.text("Added line 2");
+
+ SlotItem item = SlotItem.builder(0)
+ .material(Material.STONE)
+ .lore(List.of(initialLore))
+ .addLore(addedLine1)
+ .addLore(addedLine2)
+ .build();
+
+ assertNotNull(item.lore());
+ assertEquals(3, item.lore().size());
+ assertEquals(initialLore, item.lore().get(0));
+ assertEquals(addedLine1, item.lore().get(1));
+ assertEquals(addedLine2, item.lore().get(2));
+ }
+
+ @Test
+ @DisplayName("Test addLore without initial lore")
+ void testAddLoreWithoutInitialLore() {
+ Component line = Component.text("First lore line");
+
+ SlotItem item = SlotItem.builder(0)
+ .material(Material.STONE)
+ .addLore(line)
+ .build();
+
+ assertNotNull(item.lore());
+ assertEquals(1, item.lore().size());
+ assertEquals(line, item.lore().getFirst());
+ }
+
+ @Test
+ @DisplayName("Test builder method chaining")
+ void testBuilderMethodChaining() {
+ Component name = Component.text("Chained Item");
+
+ SlotItem item = SlotItem.builder(15)
+ .itemName(name)
+ .material(Material.EMERALD)
+ .quantity(32)
+ .customModelData(5678)
+ .build();
+
+ assertEquals(name, item.itemName());
+ assertEquals(15, item.itemSlot());
+ assertEquals(Material.EMERALD, item.material());
+ assertEquals(32, item.quantity());
+ assertEquals(5678, item.customModelData());
+ assertNull(item.lore());
+ assertNull(item.enchantments());
+ assertNull(item.customItemStack());
+ assertNull(item.damage());
+ }
+
+ @Test
+ @DisplayName("Test builder with null values")
+ void testBuilderWithNullValues() {
+ SlotItem item = SlotItem.builder(20)
+ .itemName(null)
+ .material(Material.AIR)
+ .lore(null)
+ .enchantments(null)
+ .customItemStack(null)
+ .customModelData(null)
+ .damage(null)
+ .build();
+
assertNull(item.itemName());
+ assertEquals(20, item.itemSlot());
+ assertEquals(Material.AIR, item.material());
+ assertEquals(1, item.quantity()); // Default quantity
+ assertNull(item.lore());
+ assertNull(item.enchantments());
+ assertNull(item.customItemStack());
+ assertNull(item.customModelData());
+ assertNull(item.damage());
+ }
+
+ @Test
+ @DisplayName("Test legacy constructor still works")
+ void testLegacyConstructor() {
+ Component name = Component.text("Legacy Item");
+ SlotItem item = new SlotItem(name, 5, Material.GOLD_INGOT, 16);
+
+ assertEquals(name, item.itemName());
+ assertEquals(5, item.itemSlot());
+ assertEquals(Material.GOLD_INGOT, item.material());
+ assertEquals(16, item.quantity());
+ assertNull(item.lore());
+ assertNull(item.enchantments());
+ assertNull(item.customItemStack());
+ assertNull(item.customModelData());
+ assertNull(item.damage());
+ }
+
+ @Test
+ @DisplayName("Test builder produces immutable record")
+ void testBuilderProducesImmutableRecord() {
+ Component originalName = Component.text("Original");
+ SlotItem item = SlotItem.builder(0)
+ .itemName(originalName)
+ .material(Material.DIAMOND)
+ .addLore(Component.text("Line 1"))
+ .build();
+
+ // Records are immutable, so these should work as expected
+ assertEquals(originalName, item.itemName());
+ assertEquals(Material.DIAMOND, item.material());
+
+ // Verify that the lore list is immutable if created from builder
+ assertThrows(UnsupportedOperationException.class, () -> {
+ if (item.lore() != null) {
+ item.lore().add(Component.text("This should fail"));
+ }
+ });
+ }
+
+ @Test
+ @DisplayName("Test builder with enchantments")
+ void testBuilderWithEnchantments() {
+ Map enchants = Map.of(
+ Enchantment.PROTECTION, 4,
+ Enchantment.FEATHER_FALLING, 4
+ );
+
+ SlotItem item = SlotItem.builder(8)
+ .material(Material.DIAMOND_CHESTPLATE)
+ .enchantments(enchants)
+ .build();
+
+ assertEquals(enchants, item.enchantments());
+ assertEquals(Material.DIAMOND_CHESTPLATE, item.material());
+ assertEquals(8, item.itemSlot());
+ }
+
+ @Test
+ @DisplayName("Test builder with damage value")
+ void testBuilderWithDamageValue() {
+ SlotItem item = SlotItem.builder(12)
+ .material(Material.IRON_SWORD)
+ .damage(100)
+ .build();
+
+ assertEquals(100, item.damage());
+ assertEquals(Material.IRON_SWORD, item.material());
+ assertEquals(12, item.itemSlot());
}
}
\ No newline at end of file