001 package org.bukkit.util; 002 003 import org.bukkit.ChatColor; 004 005 import java.util.LinkedList; 006 import java.util.List; 007 008 /** 009 * The ChatPaginator takes a raw string of arbitrary length and breaks it down 010 * into an array of strings appropriate for displaying on the Minecraft player 011 * console. 012 */ 013 public class ChatPaginator { 014 public static final int GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH = 55; // Will never wrap, even with the largest characters 015 public static final int AVERAGE_CHAT_PAGE_WIDTH = 65; // Will typically not wrap using an average character distribution 016 public static final int UNBOUNDED_PAGE_WIDTH = Integer.MAX_VALUE; 017 public static final int OPEN_CHAT_PAGE_HEIGHT = 20; // The height of an expanded chat window 018 public static final int CLOSED_CHAT_PAGE_HEIGHT = 10; // The height of the default chat window 019 public static final int UNBOUNDED_PAGE_HEIGHT = Integer.MAX_VALUE; 020 021 /** 022 * Breaks a raw string up into pages using the default width and height. 023 * 024 * @param unpaginatedString The raw string to break. 025 * @param pageNumber The page number to fetch. 026 * @return A single chat page. 027 */ 028 public static ChatPage paginate(String unpaginatedString, int pageNumber) { 029 return paginate(unpaginatedString, pageNumber, GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH, CLOSED_CHAT_PAGE_HEIGHT); 030 } 031 032 /** 033 * Breaks a raw string up into pages using a provided width and height. 034 * 035 * @param unpaginatedString The raw string to break. 036 * @param pageNumber The page number to fetch. 037 * @param lineLength The desired width of a chat line. 038 * @param pageHeight The desired number of lines in a page. 039 * @return A single chat page. 040 */ 041 public static ChatPage paginate(String unpaginatedString, int pageNumber, int lineLength, int pageHeight) { 042 String[] lines = wordWrap(unpaginatedString, lineLength); 043 044 int totalPages = lines.length / pageHeight + (lines.length % pageHeight == 0 ? 0 : 1); 045 int actualPageNumber = pageNumber <= totalPages ? pageNumber : totalPages; 046 047 int from = (actualPageNumber - 1) * pageHeight; 048 int to = from + pageHeight <= lines.length ? from + pageHeight : lines.length; 049 String[] selectedLines = Java15Compat.Arrays_copyOfRange(lines, from, to); 050 051 return new ChatPage(selectedLines, actualPageNumber, totalPages); 052 } 053 054 /** 055 * Breaks a raw string up into a series of lines. Words are wrapped using 056 * spaces as decimeters and the newline character is respected. 057 * 058 * @param rawString The raw string to break. 059 * @param lineLength The length of a line of text. 060 * @return An array of word-wrapped lines. 061 */ 062 public static String[] wordWrap(String rawString, int lineLength) { 063 // A null string is a single line 064 if (rawString == null) { 065 return new String[] {""}; 066 } 067 068 // A string shorter than the lineWidth is a single line 069 if (rawString.length() <= lineLength && !rawString.contains("\n")) { 070 return new String[] {rawString}; 071 } 072 073 char[] rawChars = (rawString + ' ').toCharArray(); // add a trailing space to trigger pagination 074 StringBuilder word = new StringBuilder(); 075 StringBuilder line = new StringBuilder(); 076 List<String> lines = new LinkedList<String>(); 077 int lineColorChars = 0; 078 079 for (int i = 0; i < rawChars.length; i++) { 080 char c = rawChars[i]; 081 082 // skip chat color modifiers 083 if (c == ChatColor.COLOR_CHAR) { 084 word.append(ChatColor.getByChar(rawChars[i + 1])); 085 lineColorChars += 2; 086 i++; // Eat the next character as we have already processed it 087 continue; 088 } 089 090 if (c == ' ' || c == '\n') { 091 if (line.length() == 0 && word.length() > lineLength) { // special case: extremely long word begins a line 092 for (String partialWord : word.toString().split("(?<=\\G.{" + lineLength + "})")) { 093 lines.add(partialWord); 094 } 095 } else if (line.length() + word.length() - lineColorChars == lineLength) { // Line exactly the correct length...newline 096 line.append(word); 097 lines.add(line.toString()); 098 line = new StringBuilder(); 099 lineColorChars = 0; 100 } else if (line.length() + 1 + word.length() - lineColorChars > lineLength) { // Line too long...break the line 101 for (String partialWord : word.toString().split("(?<=\\G.{" + lineLength + "})")) { 102 lines.add(line.toString()); 103 line = new StringBuilder(partialWord); 104 } 105 lineColorChars = 0; 106 } else { 107 if (line.length() > 0) { 108 line.append(' '); 109 } 110 line.append(word); 111 } 112 word = new StringBuilder(); 113 114 if (c == '\n') { // Newline forces the line to flush 115 lines.add(line.toString()); 116 line = new StringBuilder(); 117 } 118 } else { 119 word.append(c); 120 } 121 } 122 123 if(line.length() > 0) { // Only add the last line if there is anything to add 124 lines.add(line.toString()); 125 } 126 127 // Iterate over the wrapped lines, applying the last color from one line to the beginning of the next 128 if (lines.get(0).length() == 0 || lines.get(0).charAt(0) != ChatColor.COLOR_CHAR) { 129 lines.set(0, ChatColor.WHITE + lines.get(0)); 130 } 131 for (int i = 1; i < lines.size(); i++) { 132 final String pLine = lines.get(i-1); 133 final String subLine = lines.get(i); 134 135 char color = pLine.charAt(pLine.lastIndexOf(ChatColor.COLOR_CHAR) + 1); 136 if (subLine.length() == 0 || subLine.charAt(0) != ChatColor.COLOR_CHAR) { 137 lines.set(i, ChatColor.getByChar(color) + subLine); 138 } 139 } 140 141 return lines.toArray(new String[lines.size()]); 142 } 143 144 public static class ChatPage { 145 146 private String[] lines; 147 private int pageNumber; 148 private int totalPages; 149 150 public ChatPage(String[] lines, int pageNumber, int totalPages) { 151 this.lines = lines; 152 this.pageNumber = pageNumber; 153 this.totalPages = totalPages; 154 } 155 156 public int getPageNumber() { 157 return pageNumber; 158 } 159 160 public int getTotalPages() { 161 return totalPages; 162 } 163 164 public String[] getLines() { 165 166 return lines; 167 } 168 } 169 }