Skip to content

Commit

Permalink
Additional work on inventory GUIs
Browse files Browse the repository at this point in the history
  • Loading branch information
fullwall committed Jan 30, 2021
1 parent 774edba commit 3946f3f
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 44 deletions.
10 changes: 5 additions & 5 deletions src/main/java/net/citizensnpcs/api/gui/ClickHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;

/**
Expand All @@ -18,12 +18,12 @@
@Repeatable(ClickHandlers.class)
public @interface ClickHandler {
/**
* The slot position to handle clicks for.
* An optional filter for specific actions. Default = handle all clicks
*/
int[] slot();
InventoryAction[] filter() default {};

/**
* An optional filter for specific click types. Default = handle all clicks
* The slot position to handle clicks for.
*/
ClickType[] value() default {};
int[] slot();
}
14 changes: 14 additions & 0 deletions src/main/java/net/citizensnpcs/api/gui/InjectContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.citizensnpcs.api.gui;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marker annotation to inject context variables at runtime into {@link InventoryMenuPage}s.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface InjectContext {
}
106 changes: 84 additions & 22 deletions src/main/java/net/citizensnpcs/api/gui/InventoryMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
import java.util.Queue;
import java.util.WeakHashMap;

import javax.inject.Inject;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Event.Result;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
Expand All @@ -32,7 +31,9 @@
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;

// TODO: injection
import net.citizensnpcs.api.util.Colorizer;

// TODO: class-based injection? runnables? sub-inventory pages
/**
* A container class for Inventory GUIs. Expects {@link #onInventoryClick(InventoryClickEvent)} and
* {@link #onInventoryClose(InventoryCloseEvent)} to be called by the user (or registered with the event listener
Expand All @@ -51,27 +52,42 @@
* {@link InventoryMenuPage}s can either annotate specific instances of these concrete classes which will be injected at
* runtime or simply place them at the method/class level.
*
* Instances of global/contextual variables can be injected dynamically via {@link javax.inject.Inject} which sources
* Instances of global/contextual variables can be injected dynamically via {@link InjectContext} which sources
* variables from the {@link MenuContext}.
*/
public class InventoryMenu implements Listener {
private PageContext page;
private final Queue<PageContext> stack = Queues.newArrayDeque();
private Collection<InventoryView> views = Lists.newArrayList();

public InventoryMenu(InventoryMenuInfo info, InventoryMenuPage instance) {
transition(info, instance, Maps.newHashMap());
}

private InventoryMenu(InventoryMenuInfo info, Map<String, Object> context) {
transition(info, context);
transition(info, info.createInstance(), context);
}

private boolean acceptFilter(ClickType needle, ClickType[] haystack) {
for (ClickType type : haystack) {
private boolean acceptFilter(InventoryAction needle, InventoryAction[] haystack) {
for (InventoryAction type : haystack) {
if (needle == type) {
return true;
}
}
return haystack.length == 0;
}

/**
* Closes the GUI and all associated viewer inventories.
*/
public void close() {
HandlerList.unregisterAll(this);
for (InventoryView view : views) {
page.page.onClose(view.getPlayer());
view.close();
}
}

private InventoryMenuSlot createSlot(int pos, MenuSlot slotInfo) {
InventoryMenuSlot slot = page.ctx.getSlot(pos);
slot.initialise(slotInfo);
Expand All @@ -80,7 +96,7 @@ private InventoryMenuSlot createSlot(int pos, MenuSlot slotInfo) {

private InventoryMenuTransition createTransition(int pos, MenuTransition transitionInfo) {
InventoryMenuSlot slot = page.ctx.getSlot(pos);
InventoryMenuTransition transition = new InventoryMenuTransition(this, slot, transitionInfo.value());
InventoryMenuTransition transition = new InventoryMenuTransition(slot, transitionInfo.value());
return transition;
}

Expand Down Expand Up @@ -170,19 +186,28 @@ public void onInventoryClick(InventoryClickEvent event) {
break;
}
InventoryMenuSlot slot = page.ctx.getSlot(event.getSlot());
slot.onClick(event);
if (event.isCancelled()) {
return;
}
page.page.onClick(slot, event);
for (Invokable<ClickHandler> invokable : page.clickHandlers) {
int idx = posToIndex(page.dim, invokable.data.slot());
if (event.getSlot() == idx && acceptFilter(event.getClick(), invokable.data.value())) {
if (event.getSlot() != idx)
continue;
if (acceptFilter(event.getAction(), invokable.data.filter())) {
try {
// TODO: bind optional args?
invokable.method.invoke(page.page, slot, event);
} catch (Throwable e) {
e.printStackTrace();
}
} else {
event.setCancelled(true);
event.setResult(Result.DENY);
return;
}
}
slot.onClick(event);
for (InventoryMenuTransition transition : page.transitions) {
Class<? extends InventoryMenuPage> next = transition.accept(slot);
if (next != null) {
Expand Down Expand Up @@ -281,10 +306,11 @@ public void transition(Class<? extends InventoryMenuPage> clazz, Map<String, Obj
if (!CACHED_INFOS.containsKey(clazz)) {
cacheInfo(clazz);
}
transition(CACHED_INFOS.get(clazz), context);
InventoryMenuInfo info = CACHED_INFOS.get(clazz);
transition(info, info.createInstance(), context);
}

private void transition(InventoryMenuInfo info, Map<String, Object> context) {
private void transition(InventoryMenuInfo info, InventoryMenuPage instance, Map<String, Object> context) {
if (page != null) {
context.putAll(page.ctx.data());
page.ctx.data().clear();
Expand All @@ -296,23 +322,19 @@ private void transition(InventoryMenuInfo info, Map<String, Object> context) {
int size = getInventorySize(type, dim);
Inventory inventory;
if (type == InventoryType.CHEST || type == null) {
inventory = Bukkit.createInventory(null, size, info.menuAnnotation.title());
inventory = Bukkit.createInventory(null, size, Colorizer.parseColors(info.menuAnnotation.title()));
} else {
inventory = Bukkit.createInventory(null, type, info.menuAnnotation.title());
inventory = Bukkit.createInventory(null, type, Colorizer.parseColors(info.menuAnnotation.title()));
}
List<InventoryMenuTransition> transitions = Lists.newArrayList();
InventoryMenuSlot[] slots = new InventoryMenuSlot[inventory.getSize()];
page.patterns = new InventoryMenuPattern[info.patterns.length];
try {
page.page = info.constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
page.page = instance;
page.dim = dim;
page.ctx = new MenuContext(this, slots, inventory, context);
for (int i = 0; i < info.slots.length; i++) {
Bindable<MenuSlot> slotInfo = info.slots[i];
int pos = posToIndex(dim, slotInfo.data.value());
int pos = posToIndex(dim, slotInfo.data.slot());
InventoryMenuSlot slot = createSlot(pos, slotInfo.data);
slotInfo.bind(page.page, slot);
}
Expand All @@ -336,6 +358,27 @@ private void transition(InventoryMenuInfo info, Map<String, Object> context) {
transitionViewersToInventory(inventory);
}

/**
* Transition to another page. Adds the previous page to a stack which will be returned to when the current page is
* closed.
*/
public void transition(InventoryMenuPage instance) {
transition(instance, Maps.newHashMap());
}

/**
* Transition to another page with context. Adds the previous page to a stack which will be returned to when the
* current page is closed.
*/
public void transition(InventoryMenuPage instance, Map<String, Object> context) {
Class<? extends InventoryMenuPage> clazz = instance.getClass();
if (!CACHED_INFOS.containsKey(clazz)) {
cacheInfo(clazz);
}
InventoryMenuInfo info = CACHED_INFOS.get(clazz);
transition(info, instance, context);
}

private void transitionViewersToInventory(Inventory inventory) {
Collection<InventoryView> old = views;
views = Lists.newArrayListWithExpectedSize(old.size());
Expand Down Expand Up @@ -384,6 +427,14 @@ public InventoryMenuInfo(Class<?> clazz) {
injectables = getInjectables(clazz);
}

public InventoryMenuPage createInstance() {
try {
return constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@SuppressWarnings({ "unchecked" })
private <T extends Annotation> Bindable<T>[] getBindables(Class<?> clazz, Class<T> annotationType,
Class<?> concreteType) {
Expand Down Expand Up @@ -439,7 +490,7 @@ private Map<String, MethodHandle> getInjectables(Class<?> clazz) {
Map<String, MethodHandle> injectables = Maps.newHashMap();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (!field.isAnnotationPresent(Inject.class))
if (!field.isAnnotationPresent(InjectContext.class))
continue;
try {
injectables.put(field.getName(), LOOKUP.unreflectSetter(field));
Expand Down Expand Up @@ -508,6 +559,17 @@ public static InventoryMenu create(Class<? extends InventoryMenuPage> clazz) {
return createWithContext(clazz, Maps.newHashMap());
}

/**
* Create an inventory menu instance starting at the given page.
*/
public static InventoryMenu create(InventoryMenuPage instance) {
Class<? extends InventoryMenuPage> clazz = instance.getClass();
if (!CACHED_INFOS.containsKey(clazz)) {
cacheInfo(clazz);
}
return new InventoryMenu(CACHED_INFOS.get(clazz), instance);
}

/**
* Create an inventory menu instance starting at the given page and with the initial context.
*/
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/citizensnpcs/api/gui/InventoryMenuPattern.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,24 @@ public InventoryMenuPattern(MenuPattern info, Collection<InventoryMenuSlot> slot
this.transitions = transitions;
}

/**
* @return The pattern string.
*/
public String getPattern() {
return info.value();
}

/**
* @return The set of {@link InventoryMenuSlot}s that this pattern refers to.
*/
public Collection<InventoryMenuSlot> getSlots() {
return slots;
}

/**
*
* @return The set of {@link InventoryMenuTransition}s that this pattern refers to.
*/
public Collection<InventoryMenuTransition> getTransitions() {
return transitions;
}
Expand Down
59 changes: 51 additions & 8 deletions src/main/java/net/citizensnpcs/api/gui/InventoryMenuSlot.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@
import java.util.EnumSet;
import java.util.Set;

import org.bukkit.event.Event.Result;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;

import net.citizensnpcs.api.util.Colorizer;

/**
* Represents a single inventory slot in a {@link InventoryMenu}.
*/
public class InventoryMenuSlot {
private Set<ClickType> clickFilter = EnumSet.allOf(ClickType.class);
private Set<InventoryAction> actionFilter = EnumSet.allOf(InventoryAction.class);
private final int index;
private final Inventory inventory;

public InventoryMenuSlot(MenuContext menu, int i) {
InventoryMenuSlot(MenuContext menu, int i) {
this.inventory = menu.getInventory();
this.index = i;
}
Expand All @@ -42,28 +50,63 @@ public boolean equals(Object obj) {
return true;
}

/**
* @return The set of {@link InventoryAction}s that will be allowed
*/
public Collection<InventoryAction> getFilter() {
return actionFilter;
}

@Override
public int hashCode() {
int result = 31 + index;
return 31 * result + ((inventory == null) ? 0 : inventory.hashCode());
}

public void initialise(MenuSlot data) {
void initialise(MenuSlot data) {
ItemStack defaultItem = null;
if (data.material() != null) {
defaultItem = new ItemStack(data.material(), data.amount());
}
if (defaultItem != null) {
ItemMeta meta = defaultItem.getItemMeta();
if (!data.lore().isEmpty()) {
meta.setLore(Arrays.asList(Colorizer.parseColors(data.lore()).split("\\n|\n")));
}
if (!data.title().isEmpty()) {
meta.setDisplayName(Colorizer.parseColors(data.title()));
}
defaultItem.setItemMeta(meta);
}
inventory.setItem(index, defaultItem);
setClickFilter(Arrays.asList(data.filter()));
setFilter(Arrays.asList(data.filter()));
}

public void onClick(InventoryClickEvent event) {
if (!clickFilter.contains(event.getClick())) {
void onClick(InventoryClickEvent event) {
if (!actionFilter.contains(event.getAction())) {
event.setCancelled(true);
event.setResult(Result.DENY);
}
}

public void setClickFilter(Collection<ClickType> filter) {
this.clickFilter = filter == null || filter.isEmpty() ? EnumSet.allOf(ClickType.class) : EnumSet.copyOf(filter);
/**
* Sets a new {@link ClickType} filter that will only accept clicks with the given type. An empty set is equivalent
* to allowing all click types.
*
* @param filter
* The new filter
*/
public void setFilter(Collection<InventoryAction> filter) {
this.actionFilter = filter == null || filter.isEmpty() ? EnumSet.allOf(InventoryAction.class)
: EnumSet.copyOf(filter);
}

/**
* Manually set the {@link ItemStack} for this slot
*
* @param stack
*/
public void setItemStack(ItemStack stack) {
inventory.setItem(index, stack);
}
}
Loading

0 comments on commit 3946f3f

Please sign in to comment.