package org.bukkit.util; import org.bukkit.ChatColor; import java.util.LinkedList; import java.util.List; /** * The ChatPaginator takes a raw string of arbitrary length and breaks it down * into an array of strings appropriate for displaying on the Minecraft player * console. */ public class ChatPaginator { public static final int GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH = 55; // Will never wrap, even with the largest characters public static final int AVERAGE_CHAT_PAGE_WIDTH = 65; // Will typically not wrap using an average character distribution public static final int UNBOUNDED_PAGE_WIDTH = Integer.MAX_VALUE; public static final int OPEN_CHAT_PAGE_HEIGHT = 20; // The height of an expanded chat window public static final int CLOSED_CHAT_PAGE_HEIGHT = 10; // The height of the default chat window public static final int UNBOUNDED_PAGE_HEIGHT = Integer.MAX_VALUE; /** * Breaks a raw string up into pages using the default width and height. * * @param unpaginatedString The raw string to break. * @param pageNumber The page number to fetch. * @return A single chat page. */ public static ChatPage paginate(String unpaginatedString, int pageNumber) { return paginate(unpaginatedString, pageNumber, GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH, CLOSED_CHAT_PAGE_HEIGHT); } /** * Breaks a raw string up into pages using a provided width and height. * * @param unpaginatedString The raw string to break. * @param pageNumber The page number to fetch. * @param lineLength The desired width of a chat line. * @param pageHeight The desired number of lines in a page. * @return A single chat page. */ public static ChatPage paginate(String unpaginatedString, int pageNumber, int lineLength, int pageHeight) { String[] lines = wordWrap(unpaginatedString, lineLength); int totalPages = lines.length / pageHeight + (lines.length % pageHeight == 0 ? 0 : 1); int actualPageNumber = pageNumber <= totalPages ? pageNumber : totalPages; int from = (actualPageNumber - 1) * pageHeight; int to = from + pageHeight <= lines.length ? from + pageHeight : lines.length; String[] selectedLines = Java15Compat.Arrays_copyOfRange(lines, from, to); return new ChatPage(selectedLines, actualPageNumber, totalPages); } /** * Breaks a raw string up into a series of lines. Words are wrapped using * spaces as decimeters and the newline character is respected. * * @param rawString The raw string to break. * @param lineLength The length of a line of text. * @return An array of word-wrapped lines. */ public static String[] wordWrap(String rawString, int lineLength) { // A null string is a single line if(rawString == null) { return new String[]{""}; } // A string shorter than the lineWidth is a single line if(rawString.length() <= lineLength && !rawString.contains("\n")) { return new String[]{rawString}; } char[] rawChars = (rawString + ' ').toCharArray(); // add a trailing space to trigger pagination StringBuilder word = new StringBuilder(); StringBuilder line = new StringBuilder(); List<String> lines = new LinkedList<String>(); int lineColorChars = 0; for(int i = 0; i < rawChars.length; i++) { char c = rawChars[i]; // skip chat color modifiers if(c == ChatColor.COLOR_CHAR) { word.append(ChatColor.getByChar(rawChars[i + 1])); lineColorChars += 2; i++; // Eat the next character as we have already processed it continue; } if(c == ' ' || c == '\n') { if(line.length() == 0 && word.length() > lineLength) { // special case: extremely long word begins a line for(String partialWord : word.toString().split("(?<=\\G.{" + lineLength + "})")) { lines.add(partialWord); } } else if(line.length() + word.length() - lineColorChars == lineLength) { // Line exactly the correct length...newline line.append(word); lines.add(line.toString()); line = new StringBuilder(); lineColorChars = 0; } else if(line.length() + 1 + word.length() - lineColorChars > lineLength) { // Line too long...break the line for(String partialWord : word.toString().split("(?<=\\G.{" + lineLength + "})")) { lines.add(line.toString()); line = new StringBuilder(partialWord); } lineColorChars = 0; } else { if(line.length() > 0) { line.append(' '); } line.append(word); } word = new StringBuilder(); if(c == '\n') { // Newline forces the line to flush lines.add(line.toString()); line = new StringBuilder(); } } else { word.append(c); } } if(line.length() > 0) { // Only add the last line if there is anything to add lines.add(line.toString()); } // Iterate over the wrapped lines, applying the last color from one line to the beginning of the next if(lines.get(0).length() == 0 || lines.get(0).charAt(0) != ChatColor.COLOR_CHAR) { lines.set(0, ChatColor.WHITE + lines.get(0)); } for(int i = 1; i < lines.size(); i++) { final String pLine = lines.get(i - 1); final String subLine = lines.get(i); char color = pLine.charAt(pLine.lastIndexOf(ChatColor.COLOR_CHAR) + 1); if(subLine.length() == 0 || subLine.charAt(0) != ChatColor.COLOR_CHAR) { lines.set(i, ChatColor.getByChar(color) + subLine); } } return lines.toArray(new String[lines.size()]); } public static class ChatPage { private String[] lines; private int pageNumber; private int totalPages; public ChatPage(String[] lines, int pageNumber, int totalPages) { this.lines = lines; this.pageNumber = pageNumber; this.totalPages = totalPages; } public int getPageNumber() { return pageNumber; } public int getTotalPages() { return totalPages; } public String[] getLines() { return lines; } } }