Add pagination to MinecraftHelp, move minecraft-extras to it's own package

This commit is contained in:
jmp 2020-10-10 16:15:27 -07:00 committed by Alexander Söderberg
parent 454ceb318a
commit 674507fea6
7 changed files with 440 additions and 92 deletions

View file

@ -21,7 +21,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
// //
package cloud.commandframework; package cloud.commandframework.minecraft.extras;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;

View file

@ -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;
}
}

View file

@ -21,8 +21,9 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
// //
package cloud.commandframework; package cloud.commandframework.minecraft.extras;
import cloud.commandframework.CommandManager;
import cloud.commandframework.exceptions.ArgumentParseException; import cloud.commandframework.exceptions.ArgumentParseException;
import cloud.commandframework.exceptions.InvalidCommandSenderException; import cloud.commandframework.exceptions.InvalidCommandSenderException;
import cloud.commandframework.exceptions.InvalidSyntaxException; import cloud.commandframework.exceptions.InvalidSyntaxException;
@ -35,7 +36,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Pattern;
/** /**
* Exception handler that sends {@link Component} to the sender. All component builders * 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<C> { public final class MinecraftExceptionHandler<C> {
private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[^\\s\\w\\-]");
/** /**
* Default component builder for {@link InvalidSyntaxException} * Default component builder for {@link InvalidSyntaxException}
*/ */
public static final Function<Exception, Component> DEFAULT_INVALID_SYNTAX_FUNCTION = public static final Function<Exception, Component> DEFAULT_INVALID_SYNTAX_FUNCTION =
e -> Component.text() e -> Component.text()
.append(Component.text("Invalid command syntax. Correct command syntax is: ", NamedTextColor.RED)) .append(Component.text("Invalid command syntax. Correct command syntax is: ", NamedTextColor.RED))
.append(Component.text( .append(ComponentHelper.highlight(
Component.text(
String.format("/%s", ((InvalidSyntaxException) e).getCorrectSyntax()), String.format("/%s", ((InvalidSyntaxException) e).getCorrectSyntax()),
NamedTextColor.GRAY NamedTextColor.GRAY
).replaceText(SPECIAL_CHARACTERS_PATTERN, match -> match.color(NamedTextColor.WHITE))) ),
NamedTextColor.WHITE
))
.build(); .build();
/** /**
* Default component builder for {@link InvalidCommandSenderException} * Default component builder for {@link InvalidCommandSenderException}
@ -130,6 +131,19 @@ public final class MinecraftExceptionHandler<C> {
return this; return this;
} }
/**
* Use all four of the default exception handlers
*
* @return {@code this}
*/
public @NonNull MinecraftExceptionHandler<C> withDefaultHandlers() {
return this
.withArgumentParsingHandler()
.withInvalidSenderHandler()
.withInvalidSyntaxHandler()
.withNoPermissionHandler();
}
/** /**
* Specify an exception handler * Specify an exception handler
* *

View file

@ -21,8 +21,10 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
// //
package cloud.commandframework; package cloud.commandframework.minecraft.extras;
import cloud.commandframework.CommandHelpHandler;
import cloud.commandframework.CommandManager;
import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.CommandArgument;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component; 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 net.kyori.adventure.text.format.TextDecoration;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.regex.Pattern;
/** /**
* Opinionated extension of {@link CommandHelpHandler} for Minecraft * Opinionated extension of {@link CommandHelpHandler} for Minecraft
@ -48,6 +51,12 @@ import java.util.regex.Pattern;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class MinecraftHelp<C> { public final class MinecraftHelp<C> {
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( public static final HelpColors DEFAULT_HELP_COLORS = HelpColors.of(
NamedTextColor.GOLD, NamedTextColor.GOLD,
NamedTextColor.GREEN, NamedTextColor.GREEN,
@ -56,26 +65,29 @@ public final class MinecraftHelp<C> {
NamedTextColor.DARK_GRAY 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_COMMAND = "command";
public static final String MESSAGE_DESCRIPTION = "description"; public static final String MESSAGE_DESCRIPTION = "description";
public static final String MESSAGE_NO_DESCRIPTION = "no_description"; public static final String MESSAGE_NO_DESCRIPTION = "no_description";
public static final String MESSAGE_ARGUMENTS = "arguments"; public static final String MESSAGE_ARGUMENTS = "arguments";
public static final String MESSAGE_OPTIONAL = "optional"; 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_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_AVAILABLE_COMMANDS = "available_commands";
public static final String MESSAGE_CLICK_TO_SHOW_HELP = "click_to_show_help"; public static final String MESSAGE_CLICK_TO_SHOW_HELP = "click_to_show_help";
public static final String MESSAGE_PAGE_OUT_OF_RANGE = "page_out_of_range";
private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[^\\s\\w\\-]"); 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<C> audienceProvider; private final AudienceProvider<C> audienceProvider;
private final CommandManager<C> commandManager; private final CommandManager<C> commandManager;
private final String commandPrefix; private final String commandPrefix;
private final Map<String, String> messageMap = new HashMap<>(); private final Map<String, String> messageMap = new HashMap<>();
private HelpColors colors = DEFAULT_HELP_COLORS;
private BiFunction<C, String, String> messageProvider = (sender, key) -> this.messageMap.get(key); private BiFunction<C, String, String> 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 * Construct a new Minecraft help instance
@ -94,16 +106,19 @@ public final class MinecraftHelp<C> {
this.commandManager = commandManager; this.commandManager = commandManager;
/* Default Messages */ /* 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_COMMAND, "Command");
this.messageMap.put(MESSAGE_DESCRIPTION, "Description"); this.messageMap.put(MESSAGE_DESCRIPTION, "Description");
this.messageMap.put(MESSAGE_NO_DESCRIPTION, "No description"); this.messageMap.put(MESSAGE_NO_DESCRIPTION, "No description");
this.messageMap.put(MESSAGE_ARGUMENTS, "Arguments"); this.messageMap.put(MESSAGE_ARGUMENTS, "Arguments");
this.messageMap.put(MESSAGE_OPTIONAL, "Optional"); 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_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_AVAILABLE_COMMANDS, "Available Commands");
this.messageMap.put(MESSAGE_CLICK_TO_SHOW_HELP, "Click to show help for this command"); 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 <page> is not in range. Must be in range [1, <max_pages>]");
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<C> {
this.messageProvider = messageProvider; 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. * Set the colors to use for help messages.
* *
@ -177,108 +183,169 @@ public final class MinecraftHelp<C> {
} }
/** /**
* Query commands and send the results to the recipient * Set the length of the header/footer of help menus
* <p>
* 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
* <p>
* 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 * @param recipient Recipient
*/ */
public void queryCommands( public void queryCommands(
final @NonNull String query, final @NonNull String rawQuery,
final @NonNull C recipient 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); final Audience audience = this.getAudience(recipient);
audience.sendMessage(this.line(13) this.printTopic(
.append(Component.text(" " + this.messageProvider.apply(recipient, MESSAGE_HELP) + " ", this.colors.highlight)) recipient,
.append(this.line(13)) 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( private void printTopic(
final @NonNull C sender, final @NonNull C sender,
final @NonNull String query, final @NonNull String query,
final int page,
final CommandHelpHandler.@NonNull HelpTopic<C> helpTopic final CommandHelpHandler.@NonNull HelpTopic<C> 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) { if (helpTopic instanceof CommandHelpHandler.IndexHelpTopic) {
this.printIndexHelpTopic(sender, (CommandHelpHandler.IndexHelpTopic<C>) helpTopic); this.printIndexHelpTopic(sender, query, page, (CommandHelpHandler.IndexHelpTopic<C>) helpTopic);
} else if (helpTopic instanceof CommandHelpHandler.MultiHelpTopic) { } else if (helpTopic instanceof CommandHelpHandler.MultiHelpTopic) {
this.printMultiHelpTopic(sender, (CommandHelpHandler.MultiHelpTopic<C>) helpTopic); this.printMultiHelpTopic(sender, query, page, (CommandHelpHandler.MultiHelpTopic<C>) helpTopic);
} else if (helpTopic instanceof CommandHelpHandler.VerboseHelpTopic) { } else if (helpTopic instanceof CommandHelpHandler.VerboseHelpTopic) {
this.printVerboseHelpTopic(sender, (CommandHelpHandler.VerboseHelpTopic<C>) helpTopic); this.printVerboseHelpTopic(sender, query, (CommandHelpHandler.VerboseHelpTopic<C>) helpTopic);
} else { } else {
throw new IllegalArgumentException(this.messageProvider.apply(sender, MESSAGE_UNKNOWN_HELP_TOPIC_TYPE)); throw new IllegalArgumentException("Unknown help topic type");
} }
} }
private void printIndexHelpTopic( private void printIndexHelpTopic(
final @NonNull C sender, final @NonNull C sender,
final @NonNull String query,
final int page,
final CommandHelpHandler.@NonNull IndexHelpTopic<C> helpTopic final CommandHelpHandler.@NonNull IndexHelpTopic<C> helpTopic
) { ) {
final Audience audience = this.getAudience(sender); final Audience audience = this.getAudience(sender);
audience.sendMessage(lastBranch() 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<CommandHelpHandler.VerboseHelpEntry<C>>(
(currentPage, maxPages) -> {
final List<Component> header = new ArrayList<>();
header.add(this.paginatedHeader(sender, currentPage, maxPages));
header.add(this.showingResults(sender, query));
header.add(this.lastBranch()
.append(Component.text( .append(Component.text(
String.format(" %s:", this.messageProvider.apply(sender, MESSAGE_AVAILABLE_COMMANDS)), String.format(" %s:", this.messageProvider.apply(sender, MESSAGE_AVAILABLE_COMMANDS)),
this.colors.text this.colors.text
))); )));
final Iterator<CommandHelpHandler.VerboseHelpEntry<C>> iterator = helpTopic.getEntries().iterator(); return header;
while (iterator.hasNext()) { },
final CommandHelpHandler.VerboseHelpEntry<C> entry = iterator.next(); (helpEntry, isLastOfPage) -> {
final String description = helpEntry.getDescription().isEmpty()
final String description = entry.getDescription().isEmpty()
? this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP) ? this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP)
: entry.getDescription(); : helpEntry.getDescription();
Component message = Component.text(" ") final boolean lastBranch =
.append(iterator.hasNext() ? branch() : lastBranch()) isLastOfPage || helpTopic.getEntries().indexOf(helpEntry) == helpTopic.getEntries().size() - 1;
.append(this.highlight(Component.text(String.format(" /%s", entry.getSyntaxString()), this.colors.highlight))
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)) .hoverEvent(Component.text(description, this.colors.text))
.clickEvent(ClickEvent.runCommand(this.commandPrefix + ' ' + entry.getSyntaxString()))); .clickEvent(ClickEvent.runCommand(this.commandPrefix + " " + helpEntry.getSyntaxString())));
},
audience.sendMessage(message); (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( private void printMultiHelpTopic(
final @NonNull C sender, final @NonNull C sender,
final @NonNull String query,
final int page,
final CommandHelpHandler.@NonNull MultiHelpTopic<C> helpTopic final CommandHelpHandler.@NonNull MultiHelpTopic<C> helpTopic
) { ) {
final Audience audience = this.getAudience(sender); 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 int headerIndentation = helpTopic.getLongestPath().length();
final Iterator<String> iterator = helpTopic.getChildSuggestions().iterator(); new Pagination<String>(
while (iterator.hasNext()) { (currentPage, maxPages) -> {
final String suggestion = iterator.next(); final List<Component> 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(); return ComponentHelper.repeat(Component.space(), headerIndentation)
for (int i = 0; i < headerIndentation; i++) { .append(lastBranch ? this.lastBranch() : this.branch())
indentation.append(" ");
}
audience.sendMessage(
Component.text(indentation.toString())
.append(iterator.hasNext() ? this.branch() : this.lastBranch())
.append(this.highlight(Component.text(" /" + suggestion, this.colors.highlight)) .append(this.highlight(Component.text(" /" + suggestion, this.colors.highlight))
.hoverEvent(Component.text( .hoverEvent(Component.text(
this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP), this.messageProvider.apply(sender, MESSAGE_CLICK_TO_SHOW_HELP),
this.colors.text 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( private void printVerboseHelpTopic(
final @NonNull C sender, final @NonNull C sender,
final @NonNull String query,
final CommandHelpHandler.@NonNull VerboseHelpTopic<C> helpTopic final CommandHelpHandler.@NonNull VerboseHelpTopic<C> helpTopic
) { ) {
final Audience audience = this.getAudience(sender); final Audience audience = this.getAudience(sender);
audience.sendMessage(this.basicHeader(sender));
audience.sendMessage(this.showingResults(sender, query));
final String command = this.commandManager.getCommandSyntaxFormatter() final String command = this.commandManager.getCommandSyntaxFormatter()
.apply(helpTopic.getCommand().getArguments(), null); .apply(helpTopic.getCommand().getArguments(), null);
audience.sendMessage( audience.sendMessage(
@ -342,14 +409,118 @@ public final class MinecraftHelp<C> {
audience.sendMessage(component); 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) { private @NonNull Component line(final int length) {
final TextComponent.Builder line = Component.text(); return ComponentHelper.repeat(
for (int i = 0; i < length; i++) { Component.text("-", this.colors.primary, TextDecoration.STRIKETHROUGH),
line.append(Component.text("-", this.colors.primary, TextDecoration.STRIKETHROUGH)); length
} );
return line.build();
} }
private @NonNull Component branch() { private @NonNull Component branch() {
@ -361,14 +532,26 @@ public final class MinecraftHelp<C> {
} }
private @NonNull Component highlight(final @NonNull Component component) { private @NonNull Component highlight(final @NonNull Component component) {
return component.replaceText( return ComponentHelper.highlight(component, this.colors.alternateHighlight);
SPECIAL_CHARACTERS_PATTERN, }
match -> match.color(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("<page>", String.valueOf(attemptedPage))
.replace("<max_pages>", 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 { public static final class HelpColors {

View file

@ -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<T> {
private final BiFunction<Integer, Integer, List<Component>> headerRenderer;
private final BiFunction<T, Boolean, Component> rowRenderer;
private final BiFunction<Integer, Integer, Component> footerRenderer;
private final BiFunction<Integer, Integer, Component> outOfRangeRenderer;
Pagination(
final @NonNull BiFunction<Integer, Integer, List<Component>> headerRenderer,
final @NonNull BiFunction<T, Boolean, Component> rowRenderer,
final @NonNull BiFunction<Integer, Integer, Component> footerRenderer,
final @NonNull BiFunction<Integer, Integer, Component> outOfRangeRenderer
) {
this.headerRenderer = headerRenderer;
this.rowRenderer = rowRenderer;
this.footerRenderer = footerRenderer;
this.outOfRangeRenderer = outOfRangeRenderer;
}
public @NonNull List<Component> render(
final @NonNull List<T> 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<Component> 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;
}
}

View file

@ -25,4 +25,4 @@
/** /**
* Minecraft extras * Minecraft extras
*/ */
package cloud.commandframework; package cloud.commandframework.minecraft.extras;

View file

@ -26,8 +26,8 @@ package cloud.commandframework.examples.bukkit;
import cloud.commandframework.Command; import cloud.commandframework.Command;
import cloud.commandframework.CommandTree; import cloud.commandframework.CommandTree;
import cloud.commandframework.Description; import cloud.commandframework.Description;
import cloud.commandframework.MinecraftExceptionHandler; import cloud.commandframework.minecraft.extras.MinecraftExceptionHandler;
import cloud.commandframework.MinecraftHelp; import cloud.commandframework.minecraft.extras.MinecraftHelp;
import cloud.commandframework.annotations.AnnotationParser; import cloud.commandframework.annotations.AnnotationParser;
import cloud.commandframework.annotations.Argument; import cloud.commandframework.annotations.Argument;
import cloud.commandframework.annotations.CommandDescription; import cloud.commandframework.annotations.CommandDescription;