diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/AudienceProvider.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/AudienceProvider.java similarity index 97% rename from cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/AudienceProvider.java rename to cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/AudienceProvider.java index f05e5bfb..c7b1391c 100644 --- a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/AudienceProvider.java +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/AudienceProvider.java @@ -21,7 +21,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // -package cloud.commandframework; +package cloud.commandframework.minecraft.extras; import net.kyori.adventure.audience.Audience; import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/ComponentHelper.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/ComponentHelper.java new file mode 100644 index 00000000..b693744d --- /dev/null +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/ComponentHelper.java @@ -0,0 +1,72 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.minecraft.extras; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.TextColor; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.regex.Pattern; + +final class ComponentHelper { + + public static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[^\\s\\w\\-]"); + + private ComponentHelper() { + } + + public static @NonNull Component highlight( + final @NonNull Component component, + final @NonNull TextColor highlightColor + ) { + return component.replaceText( + SPECIAL_CHARACTERS_PATTERN, + match -> match.color(highlightColor) + ); + } + + public static @NonNull Component repeat( + final @NonNull Component component, + final int repetitions + ) { + final TextComponent.Builder builder = Component.text(); + for (int i = 0; i < repetitions; i++) { + builder.append(component); + } + return builder.build(); + } + + public static int length(final @NonNull Component component) { + int length = 0; + if (component instanceof TextComponent) { + length += ((TextComponent) component).content().length(); + } + for (final Component child : component.children()) { + length += length(child); + } + return length; + } + +} diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/MinecraftExceptionHandler.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java similarity index 91% rename from cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/MinecraftExceptionHandler.java rename to cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java index de50d7ca..e02abdd0 100644 --- a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/MinecraftExceptionHandler.java +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java @@ -21,8 +21,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // -package cloud.commandframework; +package cloud.commandframework.minecraft.extras; +import cloud.commandframework.CommandManager; import cloud.commandframework.exceptions.ArgumentParseException; import cloud.commandframework.exceptions.InvalidCommandSenderException; import cloud.commandframework.exceptions.InvalidSyntaxException; @@ -35,7 +36,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import java.util.regex.Pattern; /** * Exception handler that sends {@link Component} to the sender. All component builders @@ -45,18 +45,19 @@ import java.util.regex.Pattern; */ public final class MinecraftExceptionHandler { - private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[^\\s\\w\\-]"); - /** * Default component builder for {@link InvalidSyntaxException} */ public static final Function DEFAULT_INVALID_SYNTAX_FUNCTION = e -> Component.text() .append(Component.text("Invalid command syntax. Correct command syntax is: ", NamedTextColor.RED)) - .append(Component.text( - String.format("/%s", ((InvalidSyntaxException) e).getCorrectSyntax()), - NamedTextColor.GRAY - ).replaceText(SPECIAL_CHARACTERS_PATTERN, match -> match.color(NamedTextColor.WHITE))) + .append(ComponentHelper.highlight( + Component.text( + String.format("/%s", ((InvalidSyntaxException) e).getCorrectSyntax()), + NamedTextColor.GRAY + ), + NamedTextColor.WHITE + )) .build(); /** * Default component builder for {@link InvalidCommandSenderException} @@ -130,6 +131,19 @@ public final class MinecraftExceptionHandler { return this; } + /** + * Use all four of the default exception handlers + * + * @return {@code this} + */ + public @NonNull MinecraftExceptionHandler withDefaultHandlers() { + return this + .withArgumentParsingHandler() + .withInvalidSenderHandler() + .withInvalidSyntaxHandler() + .withNoPermissionHandler(); + } + /** * Specify an exception handler * diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/MinecraftHelp.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java similarity index 53% rename from cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/MinecraftHelp.java rename to cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java index d9ebc83d..66becdd2 100644 --- a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/MinecraftHelp.java +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java @@ -21,8 +21,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // -package cloud.commandframework; +package cloud.commandframework.minecraft.extras; +import cloud.commandframework.CommandHelpHandler; +import cloud.commandframework.CommandManager; import cloud.commandframework.arguments.CommandArgument; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; @@ -33,12 +35,13 @@ import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; -import java.util.regex.Pattern; /** * Opinionated extension of {@link CommandHelpHandler} for Minecraft @@ -48,6 +51,12 @@ import java.util.regex.Pattern; @SuppressWarnings("unused") public final class MinecraftHelp { + public static final int DEFAULT_HEADER_FOOTER_LENGTH = 46; + public static final int DEFAULT_MAX_RESULTS_PER_PAGE = 6; + + /** + * The default color scheme for {@link MinecraftHelp} + */ public static final HelpColors DEFAULT_HELP_COLORS = HelpColors.of( NamedTextColor.GOLD, NamedTextColor.GREEN, @@ -56,26 +65,29 @@ public final class MinecraftHelp { NamedTextColor.DARK_GRAY ); - public static final String MESSAGE_HELP = "help"; + public static final String MESSAGE_HELP_TITLE = "help"; public static final String MESSAGE_COMMAND = "command"; public static final String MESSAGE_DESCRIPTION = "description"; public static final String MESSAGE_NO_DESCRIPTION = "no_description"; public static final String MESSAGE_ARGUMENTS = "arguments"; public static final String MESSAGE_OPTIONAL = "optional"; - public static final String MESSAGE_UNKNOWN_HELP_TOPIC_TYPE = "unknown_help_topic_type"; public static final String MESSAGE_SHOWING_RESULTS_FOR_QUERY = "showing_results_for_query"; + public static final String MESSAGE_NO_RESULTS_FOR_QUERY = "no_results_for_query"; public static final String MESSAGE_AVAILABLE_COMMANDS = "available_commands"; public static final String MESSAGE_CLICK_TO_SHOW_HELP = "click_to_show_help"; - - private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[^\\s\\w\\-]"); + public static final String MESSAGE_PAGE_OUT_OF_RANGE = "page_out_of_range"; + public static final String MESSAGE_CLICK_FOR_NEXT_PAGE = "click_for_next_page"; + public static final String MESSAGE_CLICK_FOR_PREVIOUS_PAGE = "click_for_previous_page"; private final AudienceProvider audienceProvider; private final CommandManager commandManager; private final String commandPrefix; private final Map messageMap = new HashMap<>(); - private HelpColors colors = DEFAULT_HELP_COLORS; private BiFunction messageProvider = (sender, key) -> this.messageMap.get(key); + private HelpColors colors = DEFAULT_HELP_COLORS; + private int headerFooterLength = DEFAULT_HEADER_FOOTER_LENGTH; + private int maxResultsPerPage = DEFAULT_MAX_RESULTS_PER_PAGE; /** * Construct a new Minecraft help instance @@ -94,16 +106,19 @@ public final class MinecraftHelp { this.commandManager = commandManager; /* Default Messages */ - this.messageMap.put(MESSAGE_HELP, "Help"); + this.messageMap.put(MESSAGE_HELP_TITLE, "Help"); this.messageMap.put(MESSAGE_COMMAND, "Command"); this.messageMap.put(MESSAGE_DESCRIPTION, "Description"); this.messageMap.put(MESSAGE_NO_DESCRIPTION, "No description"); this.messageMap.put(MESSAGE_ARGUMENTS, "Arguments"); this.messageMap.put(MESSAGE_OPTIONAL, "Optional"); - this.messageMap.put(MESSAGE_UNKNOWN_HELP_TOPIC_TYPE, "Unknown help topic type"); this.messageMap.put(MESSAGE_SHOWING_RESULTS_FOR_QUERY, "Showing search results for query"); + this.messageMap.put(MESSAGE_NO_RESULTS_FOR_QUERY, "No results for query"); this.messageMap.put(MESSAGE_AVAILABLE_COMMANDS, "Available Commands"); this.messageMap.put(MESSAGE_CLICK_TO_SHOW_HELP, "Click to show help for this command"); + this.messageMap.put(MESSAGE_PAGE_OUT_OF_RANGE, "Error: Page is not in range. Must be in range [1, ]"); + this.messageMap.put(MESSAGE_CLICK_FOR_NEXT_PAGE, "Click for next page"); + this.messageMap.put(MESSAGE_CLICK_FOR_PREVIOUS_PAGE, "Click for previous page"); } /** @@ -158,15 +173,6 @@ public final class MinecraftHelp { this.messageProvider = messageProvider; } - /** - * Get the colors currently used for help messages. - * - * @return The current {@link HelpColors} for this {@link MinecraftHelp} instance - */ - public @NonNull HelpColors getHelpColors() { - return this.colors; - } - /** * Set the colors to use for help messages. * @@ -177,108 +183,169 @@ public final class MinecraftHelp { } /** - * Query commands and send the results to the recipient + * Set the length of the header/footer of help menus + *

+ * Defaults to {@link MinecraftHelp#DEFAULT_HEADER_FOOTER_LENGTH} * - * @param query Command query (without leading '/') + * @param headerFooterLength The new length + */ + public void setHeaderFooterLength(final int headerFooterLength) { + this.headerFooterLength = headerFooterLength; + } + + /** + * Set the maximum number of help results to display on one page + *

+ * Defaults to {@link MinecraftHelp#DEFAULT_MAX_RESULTS_PER_PAGE} + * + * @param maxResultsPerPage The new value + */ + public void setMaxResultsPerPage(final int maxResultsPerPage) { + this.maxResultsPerPage = maxResultsPerPage; + } + + /** + * Query commands and send the results to the recipient. Will respect permissions. + * + * @param rawQuery Command query (without leading '/', including optional page number) * @param recipient Recipient */ public void queryCommands( - final @NonNull String query, + final @NonNull String rawQuery, final @NonNull C recipient ) { + final String[] splitQuery = rawQuery.split(" "); + int page; + String query; + try { + final String pageText = splitQuery[splitQuery.length - 1]; + page = Integer.parseInt(pageText); + query = rawQuery.substring(0, Math.max(rawQuery.lastIndexOf(pageText) - 1, 0)); + } catch (NumberFormatException e) { + page = 1; + query = rawQuery; + } final Audience audience = this.getAudience(recipient); - audience.sendMessage(this.line(13) - .append(Component.text(" " + this.messageProvider.apply(recipient, MESSAGE_HELP) + " ", this.colors.highlight)) - .append(this.line(13)) + this.printTopic( + recipient, + query, + page, + this.commandManager.getCommandHelpHandler().queryHelp(recipient, query) ); - this.printTopic(recipient, query, this.commandManager.getCommandHelpHandler().queryHelp(recipient, query)); - audience.sendMessage(this.line(30)); } private void printTopic( final @NonNull C sender, final @NonNull String query, + final int page, final CommandHelpHandler.@NonNull HelpTopic helpTopic ) { - this.getAudience(sender).sendMessage( - Component.text(this.messageProvider.apply(sender, MESSAGE_SHOWING_RESULTS_FOR_QUERY) + ": \"", this.colors.text) - .append(this.highlight(Component.text("/" + query, this.colors.highlight))) - .append(Component.text("\"", this.colors.text)) - ); if (helpTopic instanceof CommandHelpHandler.IndexHelpTopic) { - this.printIndexHelpTopic(sender, (CommandHelpHandler.IndexHelpTopic) helpTopic); + this.printIndexHelpTopic(sender, query, page, (CommandHelpHandler.IndexHelpTopic) helpTopic); } else if (helpTopic instanceof CommandHelpHandler.MultiHelpTopic) { - this.printMultiHelpTopic(sender, (CommandHelpHandler.MultiHelpTopic) helpTopic); + this.printMultiHelpTopic(sender, query, page, (CommandHelpHandler.MultiHelpTopic) helpTopic); } else if (helpTopic instanceof CommandHelpHandler.VerboseHelpTopic) { - this.printVerboseHelpTopic(sender, (CommandHelpHandler.VerboseHelpTopic) helpTopic); + this.printVerboseHelpTopic(sender, query, (CommandHelpHandler.VerboseHelpTopic) helpTopic); } else { - throw new IllegalArgumentException(this.messageProvider.apply(sender, MESSAGE_UNKNOWN_HELP_TOPIC_TYPE)); + throw new IllegalArgumentException("Unknown help topic type"); } } private void printIndexHelpTopic( final @NonNull C sender, + final @NonNull String query, + final int page, final CommandHelpHandler.@NonNull IndexHelpTopic helpTopic ) { final Audience audience = this.getAudience(sender); - audience.sendMessage(lastBranch() - .append(Component.text( - String.format(" %s:", this.messageProvider.apply(sender, MESSAGE_AVAILABLE_COMMANDS)), - this.colors.text - ))); - final Iterator> iterator = helpTopic.getEntries().iterator(); - while (iterator.hasNext()) { - final CommandHelpHandler.VerboseHelpEntry entry = iterator.next(); - - final String description = entry.getDescription().isEmpty() - ? this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP) - : entry.getDescription(); - - Component message = Component.text(" ") - .append(iterator.hasNext() ? branch() : lastBranch()) - .append(this.highlight(Component.text(String.format(" /%s", entry.getSyntaxString()), this.colors.highlight)) - .hoverEvent(Component.text(description, this.colors.text)) - .clickEvent(ClickEvent.runCommand(this.commandPrefix + ' ' + entry.getSyntaxString()))); - - audience.sendMessage(message); + if (helpTopic.isEmpty()) { + audience.sendMessage(this.basicHeader(sender)); + audience.sendMessage(Component.text( + this.messageProvider.apply(sender, MESSAGE_NO_RESULTS_FOR_QUERY) + ": \"", + this.colors.text + ) + .append(this.highlight(Component.text("/" + query, this.colors.highlight))) + .append(Component.text("\"", this.colors.text))); + audience.sendMessage(this.footer(sender)); + return; } + new Pagination>( + (currentPage, maxPages) -> { + final List header = new ArrayList<>(); + header.add(this.paginatedHeader(sender, currentPage, maxPages)); + header.add(this.showingResults(sender, query)); + header.add(this.lastBranch() + .append(Component.text( + String.format(" %s:", this.messageProvider.apply(sender, MESSAGE_AVAILABLE_COMMANDS)), + this.colors.text + ))); + return header; + }, + (helpEntry, isLastOfPage) -> { + final String description = helpEntry.getDescription().isEmpty() + ? this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP) + : helpEntry.getDescription(); + + final boolean lastBranch = + isLastOfPage || helpTopic.getEntries().indexOf(helpEntry) == helpTopic.getEntries().size() - 1; + + return Component.text(" ") + .append(lastBranch ? this.lastBranch() : this.branch()) + .append(this.highlight(Component.text( + String.format(" /%s", helpEntry.getSyntaxString()), + this.colors.highlight + )) + .hoverEvent(Component.text(description, this.colors.text)) + .clickEvent(ClickEvent.runCommand(this.commandPrefix + " " + helpEntry.getSyntaxString()))); + }, + (currentPage, maxPages) -> this.paginatedFooter(sender, currentPage, maxPages, query), + (attemptedPage, maxPages) -> this.pageOutOfRange(sender, attemptedPage, maxPages) + ).render(helpTopic.getEntries(), page, this.maxResultsPerPage).forEach(audience::sendMessage); } private void printMultiHelpTopic( final @NonNull C sender, + final @NonNull String query, + final int page, final CommandHelpHandler.@NonNull MultiHelpTopic helpTopic ) { final Audience audience = this.getAudience(sender); - audience.sendMessage(lastBranch() - .append(this.highlight(Component.text(" /" + helpTopic.getLongestPath(), this.colors.highlight)))); final int headerIndentation = helpTopic.getLongestPath().length(); - final Iterator iterator = helpTopic.getChildSuggestions().iterator(); - while (iterator.hasNext()) { - final String suggestion = iterator.next(); + new Pagination( + (currentPage, maxPages) -> { + final List header = new ArrayList<>(); + header.add(this.paginatedHeader(sender, currentPage, maxPages)); + header.add(this.showingResults(sender, query)); + header.add(this.lastBranch() + .append(this.highlight(Component.text(" /" + helpTopic.getLongestPath(), this.colors.highlight)))); + return header; + }, + (suggestion, isLastOfPage) -> { + final boolean lastBranch = isLastOfPage + || helpTopic.getChildSuggestions().indexOf(suggestion) == helpTopic.getChildSuggestions().size() - 1; - final StringBuilder indentation = new StringBuilder(); - for (int i = 0; i < headerIndentation; i++) { - indentation.append(" "); - } - - audience.sendMessage( - Component.text(indentation.toString()) - .append(iterator.hasNext() ? this.branch() : this.lastBranch()) + return ComponentHelper.repeat(Component.space(), headerIndentation) + .append(lastBranch ? this.lastBranch() : this.branch()) .append(this.highlight(Component.text(" /" + suggestion, this.colors.highlight)) .hoverEvent(Component.text( this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP), this.colors.text )) - .clickEvent(ClickEvent.runCommand(this.commandPrefix + ' ' + suggestion))) - ); - } + .clickEvent(ClickEvent.runCommand(this.commandPrefix + " " + suggestion))); + }, + (currentPage, maxPages) -> this.paginatedFooter(sender, currentPage, maxPages, query), + (attemptedPage, maxPages) -> this.pageOutOfRange(sender, attemptedPage, maxPages) + ).render(helpTopic.getChildSuggestions(), page, this.maxResultsPerPage).forEach(audience::sendMessage); } private void printVerboseHelpTopic( final @NonNull C sender, + final @NonNull String query, final CommandHelpHandler.@NonNull VerboseHelpTopic helpTopic ) { final Audience audience = this.getAudience(sender); + audience.sendMessage(this.basicHeader(sender)); + audience.sendMessage(this.showingResults(sender, query)); final String command = this.commandManager.getCommandSyntaxFormatter() .apply(helpTopic.getCommand().getArguments(), null); audience.sendMessage( @@ -342,14 +409,118 @@ public final class MinecraftHelp { audience.sendMessage(component); } } + audience.sendMessage(this.footer(sender)); + } + + private @NonNull Component showingResults( + final @NonNull C sender, + final @NonNull String query + ) { + return Component.text(this.messageProvider.apply(sender, MESSAGE_SHOWING_RESULTS_FOR_QUERY) + ": \"", this.colors.text) + .append(this.highlight(Component.text("/" + query, this.colors.highlight))) + .append(Component.text("\"", this.colors.text)); + } + + private @NonNull Component button( + final char icon, + final @NonNull String command, + final @NonNull String hoverText + ) { + return Component.text() + .append(Component.space()) + .append(Component.text('[', this.colors.accent)) + .append(Component.text(icon, this.colors.alternateHighlight)) + .append(Component.text(']', this.colors.accent)) + .append(Component.space()) + .clickEvent(ClickEvent.runCommand(command)) + .hoverEvent(Component.text(hoverText, this.colors.text)) + .build(); + } + + private @NonNull Component footer(final @NonNull C sender) { + return this.paginatedFooter(sender, 1, 1, ""); + } + + private @NonNull Component paginatedFooter( + final @NonNull C sender, + final int currentPage, + final int maxPages, + final @NonNull String query + ) { + final boolean firstPage = currentPage == 1; + final boolean lastPage = currentPage == maxPages; + + if (firstPage && lastPage) { + return this.line(this.headerFooterLength); + } + + final String nextPageCommand = String.format("%s %s %s", this.commandPrefix, query, currentPage + 1); + final Component nextPageButton = this.button('→', nextPageCommand, + this.messageProvider.apply(sender, MESSAGE_CLICK_FOR_NEXT_PAGE) + ); + if (firstPage) { + return this.header(sender, nextPageButton); + } + + final String previousPageCommand = String.format("%s %s %s", this.commandPrefix, query, currentPage - 1); + final Component previousPageButton = this.button('←', previousPageCommand, + this.messageProvider.apply(sender, MESSAGE_CLICK_FOR_PREVIOUS_PAGE) + ); + if (lastPage) { + return this.header(sender, previousPageButton); + } + + final Component buttons = Component.text() + .append(previousPageButton) + .append(this.line(3)) + .append(nextPageButton).build(); + return this.header(sender, buttons); + } + + private @NonNull Component header( + final @NonNull C sender, + final @NonNull Component title + ) { + final int sideLength = (this.headerFooterLength - ComponentHelper.length(title)) / 2; + return Component.text() + .append(this.line(sideLength)) + .append(title) + .append(this.line(sideLength)) + .build(); + } + + private @NonNull Component basicHeader(final @NonNull C sender) { + return this.header(sender, Component.text( + " " + this.messageProvider.apply(sender, MESSAGE_HELP_TITLE) + " ", + this.colors.highlight + )); + } + + private @NonNull Component paginatedHeader( + final @NonNull C sender, + final int currentPage, + final int pages + ) { + return this.header(sender, Component.text() + .append(Component.text( + " " + this.messageProvider.apply(sender, MESSAGE_HELP_TITLE) + " ", + this.colors.highlight + )) + .append(Component.text("(", this.colors.alternateHighlight)) + .append(Component.text(currentPage, this.colors.text)) + .append(Component.text("/", this.colors.alternateHighlight)) + .append(Component.text(pages, this.colors.text)) + .append(Component.text(")", this.colors.alternateHighlight)) + .append(Component.space()) + .build() + ); } private @NonNull Component line(final int length) { - final TextComponent.Builder line = Component.text(); - for (int i = 0; i < length; i++) { - line.append(Component.text("-", this.colors.primary, TextDecoration.STRIKETHROUGH)); - } - return line.build(); + return ComponentHelper.repeat( + Component.text("-", this.colors.primary, TextDecoration.STRIKETHROUGH), + length + ); } private @NonNull Component branch() { @@ -361,14 +532,26 @@ public final class MinecraftHelp { } private @NonNull Component highlight(final @NonNull Component component) { - return component.replaceText( - SPECIAL_CHARACTERS_PATTERN, - match -> match.color(this.colors.alternateHighlight) + return ComponentHelper.highlight(component, this.colors.alternateHighlight); + } + + private @NonNull Component pageOutOfRange( + final @NonNull C sender, + final int attemptedPage, + final int maxPages + ) { + return this.highlight( + Component.text( + this.messageProvider.apply(sender, MESSAGE_PAGE_OUT_OF_RANGE) + .replace("", String.valueOf(attemptedPage)) + .replace("", String.valueOf(maxPages)), + this.colors.text + ) ); } /** - * Class for holding the {@link TextColor}s used for help menus + * Class for holding the {@link TextColor TextColors} used for help menus */ public static final class HelpColors { diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/Pagination.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/Pagination.java new file mode 100644 index 00000000..a6fa0868 --- /dev/null +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/Pagination.java @@ -0,0 +1,79 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.minecraft.extras; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + +final class Pagination { + + private final BiFunction> headerRenderer; + private final BiFunction rowRenderer; + private final BiFunction footerRenderer; + private final BiFunction outOfRangeRenderer; + + Pagination( + final @NonNull BiFunction> headerRenderer, + final @NonNull BiFunction rowRenderer, + final @NonNull BiFunction footerRenderer, + final @NonNull BiFunction outOfRangeRenderer + ) { + this.headerRenderer = headerRenderer; + this.rowRenderer = rowRenderer; + this.footerRenderer = footerRenderer; + this.outOfRangeRenderer = outOfRangeRenderer; + } + + public @NonNull List render( + final @NonNull List content, + final int page, + final int itemsPerPage + ) { + final int pages = (int) Math.ceil(content.size() / (itemsPerPage * 1.00)); + if (page < 1 || page > pages) { + return Collections.singletonList(outOfRangeRenderer.apply(page, pages)); + } + + final List renderedContent = new ArrayList<>(this.headerRenderer.apply(page, pages)); + + final int start = itemsPerPage * (page - 1); + final int maxIndex = (start + itemsPerPage); + for (int index = start; index < maxIndex; index++) { + if (index > content.size() - 1) { + break; + } + renderedContent.add(this.rowRenderer.apply(content.get(index), index == maxIndex - 1)); + } + + renderedContent.add(this.footerRenderer.apply(page, pages)); + + return renderedContent; + } + +} diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/package-info.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/package-info.java similarity index 96% rename from cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/package-info.java rename to cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/package-info.java index 78e39b61..2d0643e1 100644 --- a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/package-info.java +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/package-info.java @@ -25,4 +25,4 @@ /** * Minecraft extras */ -package cloud.commandframework; +package cloud.commandframework.minecraft.extras; diff --git a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java index b134a702..03174054 100644 --- a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java +++ b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java @@ -26,8 +26,8 @@ package cloud.commandframework.examples.bukkit; import cloud.commandframework.Command; import cloud.commandframework.CommandTree; import cloud.commandframework.Description; -import cloud.commandframework.MinecraftExceptionHandler; -import cloud.commandframework.MinecraftHelp; +import cloud.commandframework.minecraft.extras.MinecraftExceptionHandler; +import cloud.commandframework.minecraft.extras.MinecraftHelp; import cloud.commandframework.annotations.AnnotationParser; import cloud.commandframework.annotations.Argument; import cloud.commandframework.annotations.CommandDescription;