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 }