
com.github.stefvanschie.inventoryframework.Gui Maven / Gradle / Ivy
package com.github.stefvanschie.inventoryframework;
import com.github.stefvanschie.inventoryframework.pane.*;
import com.github.stefvanschie.inventoryframework.pane.component.*;
import com.github.stefvanschie.inventoryframework.util.XMLUtil;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.InventoryView;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* The base class of all GUIs
*/
public class Gui implements InventoryHolder {
/**
* A set of all panes in this inventory
*/
@NotNull
private final List panes;
/**
* The inventory of this gui
*/
@NotNull
private Inventory inventory;
/**
* The title of this gui
*/
@NotNull
private String title;
/**
* The state of this gui
*/
@NotNull
private State state = State.TOP;
/**
* A player cache for storing player's inventories
*/
@NotNull
private final HumanEntityCache humanEntityCache = new HumanEntityCache();
/**
* The consumer that will be called once a players clicks in the top-half of the gui
*/
@Nullable
private Consumer onTopClick;
/**
* The consumer that will be called once a players clicks in the bottom-half of the gui
*/
@Nullable
private Consumer onBottomClick;
/**
* The consumer that will be called once a players clicks in the gui or in their inventory
*/
@Nullable
private Consumer onGlobalClick;
/**
* The consumer that will be called once a player closes the gui
*/
@Nullable
private Consumer onClose;
/**
* The pane mapping which will allow users to register their own panes to be used in XML files
*/
@NotNull
private static final Map> PANE_MAPPINGS = new HashMap<>();
/**
* Whether listeners have ben registered by some gui
*/
private static boolean hasRegisteredListeners;
/**
* Constructs a new GUI
*
* @param plugin the main plugin.
* @param rows the amount of rows this gui should contain, in range 1..6.
* @param title the title/name of this gui.
*/
public Gui(@NotNull Plugin plugin, int rows, @NotNull String title) {
if (!(rows >= 1 && rows <= 6)) {
throw new IllegalArgumentException("Rows should be between 1 and 6");
}
this.panes = new ArrayList<>();
this.inventory = Bukkit.createInventory(this, rows * 9, title);
this.title = title;
if (!hasRegisteredListeners) {
Bukkit.getPluginManager().registerEvents(new GuiListener(), plugin);
hasRegisteredListeners = true;
}
}
/**
* Adds a pane to this gui
*
* @param pane the pane to add
*/
public void addPane(@NotNull Pane pane) {
this.panes.add(pane);
this.panes.sort(Comparator.comparing(Pane::getPriority));
}
/**
* Shows a gui to a player
*
* @param humanEntity the human entity to show the gui to
*/
public void show(@NotNull HumanEntity humanEntity) {
inventory.clear();
//set the state to the top, so in case there are no longer any bottom part panes, their inventory will be shown again
setState(State.TOP);
humanEntityCache.store(humanEntity);
for (int i = 0; i < 36; i++) {
humanEntity.getInventory().clear(i);
}
//initialize the inventory first
panes.stream().filter(Pane::isVisible).forEach(pane -> pane.display(this, inventory,
humanEntity.getInventory(), 0, 0, 9, getRows() + 4));
//ensure that the inventory is cached before being overwritten and restore it if we end up not needing the bottom part after all
if (state == State.TOP) {
humanEntityCache.restore(humanEntity);
humanEntityCache.clearCache(humanEntity);
}
humanEntity.openInventory(inventory);
}
/**
* Sets the amount of rows for this inventory.
* This will (unlike most other methods) directly update itself in order to ensure all viewers will still be viewing the new inventory as well.
*
* @param rows the amount of rows in range 1..6.
*/
public void setRows(int rows) {
if (!(rows >= 1 && rows <= 6)) {
throw new IllegalArgumentException("Rows should be between 1 and 6");
}
//copy the viewers
List viewers = new ArrayList<>(inventory.getViewers());
this.inventory = Bukkit.createInventory(this, rows * 9, getTitle());
viewers.forEach(humanEntity -> humanEntity.openInventory(inventory));
}
/**
* Gets all the panes in this gui, this includes child panes from other panes
*
* @return all panes
*/
@NotNull
@Contract(pure = true)
public List getPanes() {
List panes = new ArrayList<>();
this.panes.forEach(pane -> panes.addAll(pane.getPanes()));
panes.addAll(this.panes);
return panes;
}
/**
* Sets the title for this inventory. This will (unlike most other methods) directly update itself in order
* to ensure all viewers will still be viewing the new inventory as well.
*
* @param title the title
*/
public void setTitle(@NotNull String title) {
//copy the viewers
List viewers = new ArrayList<>(inventory.getViewers());
this.inventory = Bukkit.createInventory(this, this.inventory.getSize(), title);
this.title = title;
viewers.forEach(humanEntity -> humanEntity.openInventory(inventory));
}
/**
* Gets all the items in all underlying panes
*
* @return all items
*/
@NotNull
@Contract(pure = true)
public Collection getItems() {
return getPanes().stream().flatMap(pane -> pane.getItems().stream()).collect(Collectors.toSet());
}
/**
* Update the gui for everyone
*/
public void update() {
new HashSet<>(inventory.getViewers()).forEach(this::show);
}
/**
* Calling this method will set the state of this gui. If this state is set to top state, it will restore all the
* stored inventories of the players and will assume no pane extends into the bottom inventory part. If the state is
* set to bottom state it will assume one or more panes overflow into the bottom half of the inventory and will
* store all players' inventories and clear those.
*
* Do not call this method if you just want the player's inventory to be cleared.
*
* @param state the new gui state
* @since 0.4.0
*/
public void setState(@NotNull State state) {
this.state = state;
if (state == State.TOP) {
humanEntityCache.restoreAll();
humanEntityCache.clearCache();
} else if (state == State.BOTTOM) {
inventory.getViewers().forEach(humanEntity -> {
humanEntityCache.store(humanEntity);
for (int i = 0; i < 36; i++) {
humanEntity.getInventory().clear(i);
}
});
}
}
/**
* Gets the state of this gui
*
* @return the state
* @since 0.5.4
*/
@NotNull
@Contract(pure = true)
public State getState() {
return state;
}
/**
* Gets the human entity cache used for this gui
*
* @return the human entity cache
* @see HumanEntityCache
* @since 0.5.4
*/
@NotNull
@Contract(pure = true)
protected HumanEntityCache getHumanEntityCache() {
return humanEntityCache;
}
/**
* Loads a Gui from a given input stream
*
* @param plugin the main plugin
* @param instance the class instance for all reflection lookups
* @param inputStream the file
* @return the gui
*/
@Nullable
@Contract("_, _, null -> fail")
public static Gui load(@NotNull Plugin plugin, @NotNull Object instance, @NotNull InputStream inputStream) {
try {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream);
Element documentElement = document.getDocumentElement();
documentElement.normalize();
Gui gui = new Gui(plugin, Integer.parseInt(documentElement.getAttribute("rows")), ChatColor
.translateAlternateColorCodes('&', documentElement.getAttribute("title")));
if (documentElement.hasAttribute("field"))
XMLUtil.loadFieldAttribute(instance, documentElement, gui);
if (documentElement.hasAttribute("onTopClick")) {
Consumer onTopClickAttribute = XMLUtil.loadOnClickAttribute(instance,
documentElement, "onTopClick");
if (onTopClickAttribute != null) {
gui.setOnTopClick(onTopClickAttribute);
}
}
if (documentElement.hasAttribute("onBottomClick")) {
Consumer onBottomClickAttribute = XMLUtil.loadOnClickAttribute(instance,
documentElement, "onBottomClick");
if (onBottomClickAttribute != null) {
gui.setOnBottomClick(onBottomClickAttribute);
}
}
if (documentElement.hasAttribute("onGlobalClick")) {
Consumer onGlobalClickAttribute = XMLUtil.loadOnClickAttribute(instance,
documentElement, "onGlobalClick");
if (onGlobalClickAttribute != null) {
gui.setOnGlobalClick(onGlobalClickAttribute);
}
}
if (documentElement.hasAttribute("onClose")) {
for (Method method : instance.getClass().getMethods()) {
if (!method.getName().equals(documentElement.getAttribute("onClose")))
continue;
int parameterCount = method.getParameterCount();
if (parameterCount == 0) {
gui.setOnClose(event -> {
try {
method.setAccessible(true);
method.invoke(instance);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
});
} else if (parameterCount == 1 &&
InventoryCloseEvent.class.isAssignableFrom(method.getParameterTypes()[0])) {
gui.setOnClose(event -> {
try {
method.setAccessible(true);
method.invoke(instance, event);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
});
}
}
}
if (documentElement.hasAttribute("populate")) {
try {
Method method = instance.getClass().getMethod("populate", Gui.class);
method.setAccessible(true);
method.invoke(instance, gui);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
return gui;
}
NodeList childNodes = documentElement.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node item = childNodes.item(i);
if (item.getNodeType() != Node.ELEMENT_NODE)
continue;
gui.addPane(loadPane(instance, item));
}
return gui;
} catch (ParserConfigurationException | SAXException | IOException | NumberFormatException e) {
e.printStackTrace();
}
return null;
}
/**
* Set the consumer that should be called whenever this gui is clicked in.
*
* @param onTopClick the consumer that gets called
*/
public void setOnTopClick(@NotNull Consumer onTopClick) {
this.onTopClick = onTopClick;
}
/**
* Gets the top click event assigned to this gui, or null if there is no top click assigned.
*
* @return the top click
* @since 0.5.4
*/
@Nullable
@Contract(pure = true)
public Consumer getOnTopClick() {
return onTopClick;
}
/**
* Set the consumer that should be called whenever the inventory is clicked in.
*
* @param onBottomClick the consumer that gets called
*/
public void setOnBottomClick(@NotNull Consumer onBottomClick) {
this.onBottomClick = onBottomClick;
}
/**
* Gets the bottom click event assigned to this gui, or null if there is no bottom click assigned.
*
* @return the bottom click
* @since 0.5.4
*/
@Nullable
@Contract(pure = true)
public Consumer getOnBottomClick() {
return onBottomClick;
}
/**
* Set the consumer that should be called whenever this gui or inventory is clicked in.
*
* @param onGlobalClick the consumer that gets called
*/
public void setOnGlobalClick(@NotNull Consumer onGlobalClick) {
this.onGlobalClick = onGlobalClick;
}
/**
* Gets the global click event assigned to this gui, or null if there is no global click assigned.
*
* @return the global click
* @since 0.5.4
*/
@Nullable
@Contract(pure = true)
public Consumer getOnGlobalClick() {
return onGlobalClick;
}
/**
* Set the consumer that should be called whenever this gui is clicked in.
*
* @param onLocalClick the consumer that gets called
* @deprecated see {@link #setOnTopClick(Consumer)}
*/
@Deprecated
public void setOnLocalClick(@NotNull Consumer onLocalClick) {
this.onTopClick = onLocalClick;
}
/**
* Set the consumer that should be called whenever this gui is closed.
*
* @param onClose the consumer that gets called
*/
public void setOnClose(@NotNull Consumer onClose) {
this.onClose = onClose;
}
/**
* Gets the on close event assigned to this gui, or null if no close event is assigned.
*
* @return the on close event
* @since 0.5.4
*/
@Nullable
@Contract(pure = true)
public Consumer getOnClose() {
return onClose;
}
/**
* Returns the amount of rows this gui currently has
*
* @return the amount of rows
*/
public int getRows() {
return inventory.getSize() / 9;
}
/**
* Returns the title of this gui
*
* @return the title
*/
@NotNull
@Contract(pure = true)
public String getTitle() {
return title;
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public Inventory getInventory() {
return inventory;
}
//Code taken from InventoryView#getInventory(rawSlot) to support for 1.12 where method doesn't exist
public static Inventory getInventory(InventoryView view, int rawSlot) {
if(rawSlot == InventoryView.OUTSIDE || rawSlot == -1) {
return null;
}
if(rawSlot < view.getTopInventory().getSize()) {
return view.getTopInventory();
} else {
return view.getBottomInventory();
}
}
/**
* Registers a property that can be used inside an XML file to add additional new properties.
*
* @param attributeName the name of the property. This is the same name you'll be using to specify the property
* type in the XML file.
* @param function how the property should be processed. This converts the raw text input from the XML node value
* into the correct object type.
* @throws IllegalArgumentException when a property with this name is already registered.
*/
public static void registerProperty(@NotNull String attributeName, @NotNull Function function) {
if (Pane.getPropertyMappings().containsKey(attributeName)) {
throw new IllegalArgumentException("property '" + attributeName + "' is already registered");
}
Pane.getPropertyMappings().put(attributeName, function);
}
/**
* Registers a name that can be used inside an XML file to add custom panes
*
* @param name the name of the pane to be used in the XML file
* @param biFunction how the pane loading should be processed
* @throws IllegalArgumentException when a pane with this name is already registered
*/
public static void registerPane(@NotNull String name, @NotNull BiFunction
© 2015 - 2025 Weber Informatics LLC | Privacy Policy