diff --git a/src/main/java/com/onarandombox/MultiverseCore/commands/GamerulesCommand.java b/src/main/java/com/onarandombox/MultiverseCore/commands/GamerulesCommand.java index 62b3e060..0f0c0801 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commands/GamerulesCommand.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commands/GamerulesCommand.java @@ -8,6 +8,11 @@ package com.onarandombox.MultiverseCore.commands; import com.onarandombox.MultiverseCore.MultiverseCore; +import com.onarandombox.MultiverseCore.api.MultiverseWorld; +import com.onarandombox.MultiverseCore.displaytools.ColorAlternator; +import com.onarandombox.MultiverseCore.displaytools.ContentDisplay; +import com.onarandombox.MultiverseCore.displaytools.DisplayHandlers; +import com.onarandombox.MultiverseCore.displaytools.DisplaySettings; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.GameRule; @@ -16,7 +21,10 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.permissions.PermissionDefault; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Allows management of Anchor Destinations. @@ -68,15 +76,26 @@ public class GamerulesCommand extends MultiverseCommand { } } - final StringBuilder gameRules = new StringBuilder(); - for (final String gameRule : world.getGameRules()) { - if (gameRules.length() != 0) { - gameRules.append(ChatColor.WHITE).append(", "); + new ContentDisplay.Builder>() + .sender(sender) + .header("=== Gamerules for %s%s%s ===", ChatColor.AQUA, world.getName(), ChatColor.WHITE) + .contents(getGameRuleMap(world)) + .displayHandler(DisplayHandlers.INLINE_MAP) + .colorTool(ColorAlternator.with(ChatColor.GREEN, ChatColor.GOLD)) + .setting(DisplaySettings.OPERATOR, ": ") + .display(); + } + + private Map getGameRuleMap(World world) { + Map gameRuleMap = new HashMap<>(); + for (GameRule rule : GameRule.values()) { + Object value = world.getGameRuleValue(rule); + if (value == null) { + gameRuleMap.put(rule.getName(), "null"); + continue; } - gameRules.append(ChatColor.AQUA).append(gameRule).append(ChatColor.WHITE).append(": "); - gameRules.append(ChatColor.GREEN).append(world.getGameRuleValue(GameRule.getByName(gameRule))); + gameRuleMap.put(rule.getName(), value); } - sender.sendMessage("=== Gamerules for " + ChatColor.AQUA + world.getName() + ChatColor.WHITE + " ==="); - sender.sendMessage(gameRules.toString()); + return gameRuleMap; } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/commands/ListCommand.java b/src/main/java/com/onarandombox/MultiverseCore/commands/ListCommand.java index d702bf93..deb7b549 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commands/ListCommand.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commands/ListCommand.java @@ -7,124 +7,118 @@ package com.onarandombox.MultiverseCore.commands; +import com.dumptruckman.minecraft.util.Logging; import com.onarandombox.MultiverseCore.MultiverseCore; import com.onarandombox.MultiverseCore.api.MultiverseWorld; +import com.onarandombox.MultiverseCore.displaytools.ColorAlternator; +import com.onarandombox.MultiverseCore.displaytools.ContentDisplay; +import com.onarandombox.MultiverseCore.displaytools.ContentFilter; +import com.onarandombox.MultiverseCore.displaytools.DisplayHandlers; +import com.onarandombox.MultiverseCore.displaytools.DisplaySettings; import org.bukkit.ChatColor; -import org.bukkit.World.Environment; +import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.permissions.PermissionDefault; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * Displays a listing of all worlds that a player can enter. */ -public class ListCommand extends PaginatedCoreCommand { +public class ListCommand extends MultiverseCommand { public ListCommand(MultiverseCore plugin) { super(plugin); this.setName("World Listing"); - this.setCommandUsage("/mv list [page]"); + this.setCommandUsage("/mv list [filter] [page]"); this.setArgRange(0, 2); this.addKey("mvlist"); this.addKey("mvl"); this.addKey("mv list"); this.setPermission("multiverse.core.list.worlds", "Displays a listing of all worlds that you can enter.", PermissionDefault.OP); - this.setItemsPerPage(8); // SUPPRESS CHECKSTYLE: MagicNumberCheck - } - - private List getFancyWorldList(Player p) { - List worldList = new ArrayList(); - for (MultiverseWorld world : this.plugin.getMVWorldManager().getMVWorlds()) { - - if (p != null && (!this.plugin.getMVPerms().canEnterWorld(p, world))) { - continue; - } - - ChatColor color = ChatColor.GOLD; - Environment env = world.getEnvironment(); - if (env == Environment.NETHER) { - color = ChatColor.RED; - } else if (env == Environment.NORMAL) { - color = ChatColor.GREEN; - } else if (env == Environment.THE_END) { - color = ChatColor.AQUA; - } - StringBuilder builder = new StringBuilder(); - builder.append(world.getColoredWorldString()).append(ChatColor.WHITE); - builder.append(" - ").append(color).append(world.getEnvironment()); - if (world.isHidden()) { - if (p == null || this.plugin.getMVPerms().hasPermission(p, "multiverse.core.modify", true)) { - // Prefix hidden worlds with an "[H]" - worldList.add(ChatColor.GRAY + "[H]" + builder.toString()); - } - } else { - worldList.add(builder.toString()); - } - } - for (String name : this.plugin.getMVWorldManager().getUnloadedWorlds()) { - if (p == null || this.plugin.getMVPerms().hasPermission(p, "multiverse.access." + name, true)) { - worldList.add(ChatColor.GRAY + name + " - UNLOADED"); - } - } - return worldList; - } - - @Override - protected List getFilteredItems(List availableItems, String filter) { - List filtered = new ArrayList(); - - for (String s : availableItems) { - if (s.matches("(?i).*" + filter + ".*")) { - filtered.add(s); - } - } - return filtered; - } - - @Override - protected String getItemText(String item) { - return item; } @Override public void runCommand(CommandSender sender, List args) { - sender.sendMessage(ChatColor.LIGHT_PURPLE + "====[ Multiverse World List ]===="); - Player p = null; - if (sender instanceof Player) { - p = (Player) sender; - } + ContentFilter filter = ContentFilter.DEFAULT; + int page = 1; - - FilterObject filterObject = this.getPageAndFilter(args); - - List availableWorlds = new ArrayList(this.getFancyWorldList(p)); - if (filterObject.getFilter().length() > 0) { - availableWorlds = this.getFilteredItems(availableWorlds, filterObject.getFilter()); - if (availableWorlds.size() == 0) { - sender.sendMessage(ChatColor.RED + "Sorry... " + ChatColor.WHITE - + "No worlds matched your filter: " + ChatColor.AQUA + filterObject.getFilter()); - return; + // Either page or filter. + if (args.size() == 1) { + try { + page = Integer.parseInt(args.get(0)); + } catch (NumberFormatException ignore) { + filter = new ContentFilter(args.get(0)); } } - if (!(sender instanceof Player)) { - for (String c : availableWorlds) { - sender.sendMessage(c); + // Filter then page. + if (args.size() == 2) { + filter = new ContentFilter(args.get(0)); + try { + page = Integer.parseInt(args.get(1)); + } catch (NumberFormatException ignore) { + sender.sendMessage(ChatColor.RED + args.get(1) + " is not valid number!"); } - return; } - int totalPages = (int) Math.ceil(availableWorlds.size() / (this.itemsPerPage + 0.0)); + new ContentDisplay.Builder>() + .sender(sender) + .header("%s====[ Multiverse World List ]====", ChatColor.GOLD) + .contents(getListContents(sender)) + .displayHandler(DisplayHandlers.PAGE_LIST) + .colorTool(ColorAlternator.with(ChatColor.AQUA, ChatColor.GOLD)) + .filter(filter) + .setting(DisplaySettings.SHOW_PAGE, page) + .display(); + } - if (filterObject.getPage() > totalPages) { - filterObject.setPage(totalPages); + private List getListContents(@NotNull CommandSender sender) { + Player player = (sender instanceof Player) ? (Player) sender : null; + + List worldList = this.plugin.getMVWorldManager().getMVWorlds().stream() + .filter(world -> player == null || plugin.getMVPerms().canEnterWorld(player, world)) + .filter(world -> canSeeWorld(player, world)) + .map(world -> hiddenText(world) + world.getColoredWorldString() + " - " + parseColouredEnvironment(world.getEnvironment())) + .collect(Collectors.toList()); + + this.plugin.getMVWorldManager().getUnloadedWorlds().stream() + .filter(world -> plugin.getMVPerms().hasPermission(sender, "multiverse.access." + world, true)) + .map(world -> ChatColor.GRAY + world + " - UNLOADED") + .forEach(worldList::add); + + return worldList; + } + + private boolean canSeeWorld(Player player, MultiverseWorld world) { + return !world.isHidden() + || player == null + || this.plugin.getMVPerms().hasPermission(player, "multiverse.core.modify", true); + } + + private String hiddenText(MultiverseWorld world) { + return (world.isHidden()) ? String.format("%s[H] ", ChatColor.GRAY) : ""; + } + + private String parseColouredEnvironment(World.Environment env) { + ChatColor color = ChatColor.GOLD; + switch (env) { + case NETHER: + color = ChatColor.RED; + break; + case NORMAL: + color = ChatColor.GREEN; + break; + case THE_END: + color = ChatColor.AQUA; + break; } - - sender.sendMessage(ChatColor.AQUA + " Page " + filterObject.getPage() + " of " + totalPages); - - this.showPage(filterObject.getPage(), sender, availableWorlds); + return color + env.toString(); } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/ColorAlternator.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ColorAlternator.java new file mode 100644 index 00000000..a306ad1c --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ColorAlternator.java @@ -0,0 +1,62 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import org.bukkit.ChatColor; +import org.jetbrains.annotations.NotNull; + +/** + * Helper class to switch between 2 {@link ChatColor}. + */ +public class ColorAlternator implements ColorTool { + + /** + * Creates a new {@link ColorAlternator} with 2 {@link ChatColor}s. + * + * @param colorThis The first color. + * @param colorThat The second color. + * @return The {@link ColorAlternator} created for you. + */ + public static ColorAlternator with(@NotNull ChatColor colorThis, + @NotNull ChatColor colorThat) { + + return new ColorAlternator(colorThis, colorThat); + } + + private boolean switcher; + private final ChatColor thisColor; + private final ChatColor thatColor; + + /** + * @param colorThis The first color. + * @param colorThat The second color. + */ + public ColorAlternator(@NotNull ChatColor colorThis, + @NotNull ChatColor colorThat) { + + this.thisColor = colorThis; + this.thatColor = colorThat; + } + + /** + * Gets the color. Everytime this method is called, it swaps the color that it returns. + * + * @return The color. + */ + @Override + public ChatColor get() { + return (this.switcher ^= true) ? this.thisColor : this.thatColor; + } + + /** + * @return The first color. + */ + public ChatColor getThisColor() { + return thisColor; + } + + /** + * @return The second color. + */ + public ChatColor getThatColor() { + return thatColor; + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/ColorTool.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ColorTool.java new file mode 100644 index 00000000..69db94cc --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ColorTool.java @@ -0,0 +1,21 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import org.bukkit.ChatColor; + +/** + * Tools to allow customisation. + */ +public interface ColorTool { + + /** + * Gets a chat color. + * + * @return The color. + */ + ChatColor get(); + + /** + * Default implementation of this interface. Returns a default white color. + */ + ColorTool DEFAULT = () -> ChatColor.WHITE; +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/ContentDisplay.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ContentDisplay.java new file mode 100644 index 00000000..027340f0 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ContentDisplay.java @@ -0,0 +1,264 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; + +/** + * Helps to display contents such as list and maps in a nicely formatted fashion. + * + * @param Type of content to display. + */ +public class ContentDisplay { + + public static final String LINE_BREAK = "%br%"; + + private CommandSender sender; + private String header; + private T contents; + private String emptyMessage = "No matching content to display."; + private DisplayHandler displayHandler; + private ColorTool colorTool = ColorTool.DEFAULT; + private ContentFilter filter = ContentFilter.DEFAULT; + private final Map, Object> settingsMap = new WeakHashMap<>(); + + private ContentDisplay() { } + + /** + * Do the actual displaying of contents to the sender. + */ + public void send() { + Collection formattedContent; + try { + formattedContent = (this.contents == null) ? null : this.displayHandler.format(this); + } catch (DisplayFormatException e) { + this.sender.sendMessage(String.format("%sError: %s", ChatColor.RED, e.getMessage())); + return; + } + this.displayHandler.sendHeader(this); + this.displayHandler.sendSubHeader(this); + this.displayHandler.sendBody(this, formattedContent); + } + + /** + * @return Gets the target sender. + */ + @NotNull + public CommandSender getSender() { + return sender; + } + + /** + * @return Gets the header to display. + */ + public String getHeader() { + return header; + } + + /** + * Sets the header text. + */ + public void setHeader(@NotNull String header) { + this.header = header; + } + + /** + * @return Gets the contents to display. + */ + public T getContents() { + return contents; + } + + /** + * @return Gets the message to display when no content is shown. + */ + @NotNull + public String getEmptyMessage() { + return emptyMessage; + } + + /** + * @return Gets the display handler that formats and sends content to sender. + */ + @NotNull + public DisplayHandler getDisplayHandler() { + return displayHandler; + } + + /** + * @return Gets the color tool used. + */ + @NotNull + public ColorTool getColorTool() { + return colorTool; + } + + /** + * @return Gets the filter used. + */ + @NotNull + public ContentFilter getFilter() { + return filter; + } + + /** + * Gets the value for a given setting option. + * + * @param setting The setting option. + * @param The setting type. + * @return Value set for the given setting. + */ + public S getSetting(@NotNull DisplaySetting setting) { + return (S) settingsMap.getOrDefault(setting, setting.defaultValue()); + } + + /** + * Sets other specific settings that may be used by the {@link DisplayHandler}. + * + * @param setting The settings option. + * @param value The value to set. + * @param The type of setting. + */ + public void setSetting(@NotNull DisplaySetting setting, S value) { + this.settingsMap.put(setting, value); + } + + /** + * Builds a {@link ContentDisplay}. + * + * @param Type of content to display. + */ + public static class Builder { + + private final ContentDisplay display; + + public Builder() { + this.display = new ContentDisplay<>(); + } + + /** + * Sets target sender to display message to. Required. + * + * @param sender The target sender. + * @return The builder. + */ + @NotNull + public Builder sender(@NotNull CommandSender sender) { + this.display.sender = sender; + return this; + } + + /** + * Sets header to be displayed. + * + * @param header The header text. + * @param replacements String formatting replacements. + * @return The builder. + */ + @NotNull + public Builder header(@NotNull String header, Object...replacements) { + this.display.header = String.format(header, replacements); + return this; + } + + /** + * Sets content to be displayed. + * + * @param contents The contents. + * @return The builder. + */ + @NotNull + public Builder contents(@Nullable T contents) { + this.display.contents = contents; + return this; + } + + /** + * Sets the message to show when no content is available for display. + * + * @param emptyMessage The message text. + * @param replacements String formatting replacements. + * @return The builder. + */ + @NotNull + public Builder emptyMessage(@NotNull String emptyMessage, Object...replacements) { + this.display.emptyMessage = String.format(emptyMessage, replacements); + return this; + } + + /** + * Sets the display handler that does the formatting and sending of content. Required. + * + * @param displayHandler The display handler for the given content type. + * @return The builder. + */ + @NotNull + public Builder displayHandler(@NotNull DisplayHandler displayHandler) { + this.display.displayHandler = displayHandler; + return this; + } + + /** + * Sets the color tool used to make messages more colourful. + * + * @param colorTool The color tool to use. + * @return The builder. + */ + @NotNull + public Builder colorTool(@NotNull ColorTool colorTool) { + this.display.colorTool = colorTool; + return this; + } + + /** + * Sets content filter used to match specific content to be displayed. + * + * @param filter The filter to use. + * @return The builder. + */ + @NotNull + public Builder filter(@NotNull ContentFilter filter) { + this.display.filter = filter; + return this; + } + + /** + * Sets other specific settings that may be used by the {@link DisplayHandler}. + * + * @param setting The settings option. + * @param value The value to set. + * @param The type of setting. + * @return The builder. + */ + @NotNull + public Builder setting(@NotNull DisplaySetting setting, S value) { + this.display.settingsMap.put(setting, value); + return this; + } + + /** + * Validates and build the content display. + * + * @return The content display. + */ + @NotNull + public ContentDisplay build() { + Objects.requireNonNull(this.display.sender); + Objects.requireNonNull(this.display.displayHandler); + return this.display; + } + + /** + * Build and send the contents to the sender. + */ + public void display() { + this.build().send(); + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/ContentFilter.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ContentFilter.java new file mode 100644 index 00000000..d2f37dc5 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/ContentFilter.java @@ -0,0 +1,153 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.ChatColor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + *

Filter content and text based on regex matching.

+ * + *

Compile regex pattern based on {@link ContentFilter#filterString}. When prefixed with 'r=', + * use {@link ContentFilter#filterString} as the full regex pattern. Else, set to any match that + * contains the {@link ContentFilter#filterString}.

+ */ +public class ContentFilter { + + public static final ContentFilter DEFAULT = new ContentFilter(); + private static final Pattern REGEX_SPECIAL_CHARS = Pattern.compile("[.+*?\\[^\\]$(){}=!<>|:-\\\\]"); + + private String filterString; + private Pattern filterPattern; + private boolean exactMatch; + + private ContentFilter() { + } + + /** + * @param filterString The text to do matching, either plaintext or regex. + */ + public ContentFilter(@NotNull String filterString) { + this(filterString, false); + } + + /** + * @param filterString The text to do matching, else plaintext or regex. + * @param exactMatch Should check for exact match when doing regex matching. + */ + public ContentFilter(@NotNull String filterString, + boolean exactMatch) { + + this.filterString = filterString; + this.exactMatch = exactMatch; + parseFilter(); + } + + private void parseFilter() { + if (filterString == null) { + return; + } + if (filterString.startsWith("r=")) { + convertToMatcher(filterString.substring(2)); + return; + } + String cleanedFilter = REGEX_SPECIAL_CHARS.matcher(filterString.toLowerCase()).replaceAll("\\\\$0"); + convertToMatcher("(?i).*" + cleanedFilter + ".*"); + } + + /** + * Compile and store the regex into a {@link Pattern}. + * + * @param regex The regex text. + */ + private void convertToMatcher(@NotNull String regex) { + try { + this.filterPattern = Pattern.compile(regex); + Logging.finest("Parsed regex pattern: %s", this.filterPattern.toString()); + } + catch (PatternSyntaxException ignored) { + Logging.warning("Error parsing regex: %s", filterString); + } + } + + /** + * Do regex matching. + * + * @param text String to check regex on. + * @return True of matches regex pattern, false otherwise. + */ + public boolean checkMatch(@Nullable Object text) { + if (!hasFilter()) { + return true; + } + if (text == null || !hasValidPattern()) { + return false; + } + text = ChatColor.stripColor(String.valueOf(text)); + return (exactMatch) + ? filterPattern.matcher((CharSequence) text).matches() + : filterPattern.matcher((CharSequence) text).find(); + } + + /** + * Checks if a filter string is present. + * + * @return True if there is a filter string, else false. + */ + public boolean hasFilter() { + return filterString != null; + } + + /** + * Checks if regex pattern syntax is valid. + * + * @return True if valid, else false. + */ + public boolean hasValidPattern() { + return filterPattern != null; + } + + /** + * @return The filter string. + */ + @Nullable + public String getString() { + return filterString; + } + + /** + * @return The regex pattern. + */ + @Nullable + public Pattern getPattern() { + return filterPattern; + } + + /** + * @return True if filter is set to do exact matching, else false. + */ + public boolean isExactMatch() { + return exactMatch; + } + + /** + * Nicely format the filter string to be used for showing the sender. + * + * @return The formatted filter string. + */ + public @NotNull String getFormattedString() { + return String.format("%sFilter: '%s'", ChatColor.ITALIC, filterString); + } + + @Override + public String toString() { + return "ContentFilter{" + + "filterString='" + filterString + '\'' + + ", filterPattern=" + filterPattern + + ", exactMatch=" + exactMatch + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayFormatException.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayFormatException.java new file mode 100644 index 00000000..0f71e247 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayFormatException.java @@ -0,0 +1,25 @@ +package com.onarandombox.MultiverseCore.displaytools; + +/** + * Thrown when an issue occur while formatting content. + */ +public class DisplayFormatException extends Exception { + public DisplayFormatException() { + } + + public DisplayFormatException(String message) { + super(message); + } + + public DisplayFormatException(String message, Throwable cause) { + super(message, cause); + } + + public DisplayFormatException(Throwable cause) { + super(cause); + } + + public DisplayFormatException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayHandler.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayHandler.java new file mode 100644 index 00000000..271e351c --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayHandler.java @@ -0,0 +1,61 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import com.google.common.base.Strings; +import org.bukkit.ChatColor; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Handles the formatting and sending of all content by the {@link ContentDisplay}. + * + * @param Type of content to display. + */ +public interface DisplayHandler { + + /** + * Formats the raw content into a {@link Collection} for displaying to sender. + * + * @param display The responsible {@link ContentDisplay}. + * @return The formatted content. + * @throws DisplayFormatException Issue occurred while formatting content. E.g. invalid page. + */ + Collection format(@NotNull ContentDisplay display) throws DisplayFormatException; + + /** + * Sends the header. + * + * @param display The responsible {@link ContentDisplay}. + */ + default void sendHeader(@NotNull ContentDisplay display) { + if (!Strings.isNullOrEmpty(display.getHeader())) { + display.getSender().sendMessage(display.getHeader()); + } + } + + /** + * Sends info such as filter and page. + * + * @param display The responsible {@link ContentDisplay}. + */ + default void sendSubHeader(@NotNull ContentDisplay display) { + if (display.getFilter().hasFilter()) { + display.getSender().sendMessage(String.format("%s[ %s ]", + ChatColor.GRAY, display.getFilter().getFormattedString())); + } + } + + /** + * Sends the content. + * + * @param display The responsible {@link ContentDisplay}. + * @param formattedContent The content after being formatted by {@link #format(ContentDisplay)} + */ + default void sendBody(@NotNull ContentDisplay display, Collection formattedContent) { + if (formattedContent == null || formattedContent.size() == 0) { + display.getSender().sendMessage(display.getEmptyMessage()); + return; + } + display.getSender().sendMessage(formattedContent.toArray(new String[0])); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayHandlers.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayHandlers.java new file mode 100644 index 00000000..d7e18978 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplayHandlers.java @@ -0,0 +1,179 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import org.bukkit.ChatColor; +import org.bukkit.command.ConsoleCommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Various implementations of {@link DisplayHandler}. + */ +public class DisplayHandlers { + + /** + * Standard list display. + * + * Supported settings: none. + */ + public static final DisplayHandler> LIST = display -> display.getContents().stream() + .filter(display.getFilter()::checkMatch) + .map(s -> (ContentDisplay.LINE_BREAK.equals(s)) ? "" : display.getColorTool().get() + s) + .collect(Collectors.toList()); + + /** + * List display with paging. + * + * Supported settings: {@link DisplaySettings#SHOW_PAGE}, {@link DisplaySettings#LINES_PER_PAGE}, + * {@link DisplaySettings#PAGE_IN_CONSOLE}, {@link DisplaySettings#DO_END_PADDING}. + */ + public static final DisplayHandler> PAGE_LIST = new DisplayHandler>() { + @Override + public Collection format(@NotNull ContentDisplay> display) throws DisplayFormatException { + if (dontNeedPaging(display)) { + return LIST.format(display); + } + + int pages = 1; + int currentLength = 0; + int targetPage = display.getSetting(DisplaySettings.SHOW_PAGE); + int linesPerPage = display.getSetting(DisplaySettings.LINES_PER_PAGE); + List content = new ArrayList<>(linesPerPage); + + // Calculate the paging. + for (String line : display.getContents()) { + if (!display.getFilter().checkMatch(line)) { + continue; + } + // When it's the next page. + boolean isLineBreak = ContentDisplay.LINE_BREAK.equals(line); + if (isLineBreak || ++currentLength > linesPerPage) { + pages++; + currentLength = 0; + if (isLineBreak) { + continue; + } + } + if (pages == targetPage) { + // Let first line be the header when no header is defined. + if (display.getHeader() == null) { + display.setHeader(line); + currentLength--; + continue; + } + content.add(display.getColorTool().get() + line); + } + } + + // Page out of range. + if (targetPage < 1 || targetPage > pages) { + if (pages == 1) { + throw new DisplayFormatException("There is only 1 page!"); + } + throw new DisplayFormatException("Please enter a page from 1 to " + pages + "."); + } + + // No content + if (content.size() == 0) { + content.add(display.getEmptyMessage()); + } + + // Add empty lines to make output length consistent. + if (display.getSetting(DisplaySettings.DO_END_PADDING)) { + IntStream.range(0, linesPerPage - content.size()).forEach(i -> content.add("")); + } + display.setSetting(DisplaySettings.TOTAL_PAGE, pages); + + return content; + } + + @Override + public void sendSubHeader(@NotNull ContentDisplay> display) { + if (dontNeedPaging(display)) { + LIST.sendSubHeader(display); + return; + } + + if (display.getFilter().hasFilter()) { + display.getSender().sendMessage(String.format("%s[ Page %s of %s, %s ]", + ChatColor.GRAY, + display.getSetting(DisplaySettings.SHOW_PAGE), + display.getSetting(DisplaySettings.TOTAL_PAGE), + display.getFilter().getFormattedString()) + ); + return; + } + display.getSender().sendMessage(String.format("%s[ Page %s of %s ]", + ChatColor.GRAY, + display.getSetting(DisplaySettings.SHOW_PAGE), + display.getSetting(DisplaySettings.TOTAL_PAGE)) + ); + } + + private boolean dontNeedPaging(ContentDisplay> display) { + return display.getSender() instanceof ConsoleCommandSender + && !display.getSetting(DisplaySettings.PAGE_IN_CONSOLE); + } + }; + + /** + * Display a list inline. + * + * Supported settings: {@link DisplaySettings#SEPARATOR}. + */ + public static final DisplayHandler> INLINE_LIST = display -> { + StringBuilder builder = new StringBuilder(); + String separator = display.getSetting(DisplaySettings.SEPARATOR); + + for (Iterator iterator = display.getContents().iterator(); iterator.hasNext(); ) { + String content = iterator.next(); + if (!display.getFilter().checkMatch(content)) { + continue; + } + builder.append(display.getColorTool().get()).append(content); + if (iterator.hasNext()) { + builder.append(separator); + } + } + return (builder.length() == 0) + ? Collections.singletonList(display.getEmptyMessage()) + : Collections.singleton(builder.toString()); + }; + + /** + * Display key value pair inline. + * + * Supported settings: {@link DisplaySettings#SEPARATOR}, {@link DisplaySettings#OPERATOR}. + */ + public static final DisplayHandler> INLINE_MAP = display -> { + StringBuilder builder = new StringBuilder(); + String separator = display.getSetting(DisplaySettings.SEPARATOR); + String operator = display.getSetting(DisplaySettings.OPERATOR); + + for (Iterator> iterator = display.getContents().entrySet().iterator(); iterator.hasNext(); ) { + Entry entry = iterator.next(); + if (!display.getFilter().checkMatch(entry.getKey()) && !display.getFilter().checkMatch(entry.getValue())) { + continue; + } + builder.append(display.getColorTool().get()) + .append(entry.getKey()) + .append(operator) + .append(display.getColorTool().get()) + .append(entry.getValue()); + if (iterator.hasNext()) { + builder.append(separator); + } + } + return (builder.length() == 0) + ? Collections.singletonList(display.getEmptyMessage()) + : Collections.singleton(builder.toString()); + }; +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplaySetting.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplaySetting.java new file mode 100644 index 00000000..ff9a5e15 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplaySetting.java @@ -0,0 +1,16 @@ +package com.onarandombox.MultiverseCore.displaytools; + +/** + * Represents a setting option that can be used by {@link DisplayHandler}. + * + * @param + */ +public interface DisplaySetting { + + /** + * Gets the default value of this Display Setting. + * + * @return The default value. + */ + T defaultValue(); +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplaySettings.java b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplaySettings.java new file mode 100644 index 00000000..238de325 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/displaytools/DisplaySettings.java @@ -0,0 +1,44 @@ +package com.onarandombox.MultiverseCore.displaytools; + +import org.bukkit.ChatColor; + +/** + * Collection of {@link DisplaySetting} that are used by various {@link DisplayHandler}. + */ +public class DisplaySettings { + + /** + * Page to display. + */ + public static final DisplaySetting SHOW_PAGE = () -> 1; + + /** + * Total pages available to display. + */ + public static final DisplaySetting TOTAL_PAGE = () -> 1; + + /** + * The max number of lines per page. This excludes header. + */ + public static final DisplaySetting LINES_PER_PAGE = () -> 8; + + /** + * Should add empty lines if content lines shown is less that {@link #LINES_PER_PAGE}. + */ + public static final DisplaySetting DO_END_PADDING = () -> true; + + /** + * Should display with paging when it's sent to console. + */ + public static final DisplaySetting PAGE_IN_CONSOLE = () -> false; + + /** + * Inline separator. E.g. '1, 2, 3' + */ + public static final DisplaySetting SEPARATOR = () -> ChatColor.WHITE + ", "; + + /** + * The thing between a key value pair. E.g. 'Me = Smart' + */ + public static final DisplaySetting OPERATOR = () -> ChatColor.WHITE + " = "; +}