package me.desht.scrollingmenusign.views; import me.desht.dhutils.ConfigurationManager; import me.desht.scrollingmenusign.SMSException; import me.desht.scrollingmenusign.SMSMenu; import me.desht.scrollingmenusign.ScrollingMenuSign; import org.bukkit.entity.Player; import java.util.*; /** * Represents the abstract base class for all scrollable views. Provides per-player scroll positioning. */ public abstract class SMSScrollableView extends SMSView { public enum ScrollType { DEFAULT, SCROLL, PAGE } public static final String MAX_TITLE_LINES = "max_title_lines"; public static final String SCROLL_TYPE = "scrolltype"; private static ScrollType defaultScrollType; private boolean wrap; private final Map<UUID, Integer> playerScrollPos = new HashMap<UUID, Integer>(); private final ScrollPosStack storedScrollPos = new ScrollPosStack(); public SMSScrollableView(SMSMenu menu) { this(null, menu); } public SMSScrollableView(String name, SMSMenu menu) { super(name, menu); wrap = true; registerAttribute(MAX_TITLE_LINES, 0, "Max lines to use for menu title"); registerAttribute(SCROLL_TYPE, ScrollType.DEFAULT, "View scrolling method (scroll or page)"); } @Override public void pushMenu(Player player, SMSMenu newActive) { storedScrollPos.pushScrollPos(player, getScrollPos(player)); setScrollPos(player, 1); super.pushMenu(player, newActive); } @Override public SMSMenu popMenu(Player player) { setScrollPos(player, storedScrollPos.popScrollPos(player)); return super.popMenu(player); } public static void setDefaultScrollType(ScrollType scrollType) { defaultScrollType = scrollType; } public static ScrollType getDefaultScrollType() { return defaultScrollType; } public boolean isWrap() { return wrap; } public void setWrap(boolean wrap) { this.wrap = wrap; } /** * Get the scroll type for this view. * * @return the view's scroll type */ public ScrollType getScrollType() { ScrollType t = (ScrollType) getAttribute(SCROLL_TYPE); return t == ScrollType.DEFAULT ? getDefaultScrollType() : t; } /** * Get the given player's scroll position (currently-selected item) for this view. If the scroll position * is out of range (possibly because an item was deleted from the menu), it will be automatically * adjusted to be in range before being returned. * * @param player The player to check * @return The scroll position */ public int getScrollPos(Player player) { UUID key = getPlayerContext(player); Integer pos = playerScrollPos.get(getPlayerContext(player)); if (pos == null || pos < 1) { setScrollPos(player, 1); } else if (pos > getActiveMenuItemCount(player)) { setScrollPos(player, getActiveMenuItemCount(player)); } return playerScrollPos.get(key); } /** * Sets the scroll position for the given player on this view. * * @param player The player's name * @param scrollPos The new scroll position */ public void setScrollPos(Player player, int scrollPos) { playerScrollPos.put(getPlayerContext(player), scrollPos); setDirty(player, true); } /** * Sets the currently-selected item for the given player to the next item. * * @param player The player to scroll the view for */ public void scrollDown(Player player) { int pos = getScrollPos(player) + 1; if (pos > getActiveMenuItemCount(player)) { pos = wrap ? 1 : getActiveMenuItemCount(player); } setScrollPos(player, pos); } /** * Sets the current selected item for the given player to the previous item. * * @param player The player to scroll the view for */ public void scrollUp(Player player) { if (getActiveMenuItemCount(player) == 0) return; int pos = getScrollPos(player) - 1; if (pos <= 0) { pos = wrap ? getActiveMenuItemCount(player) : 1; } setScrollPos(player, pos); } /* (non-Javadoc) * @see me.desht.scrollingmenusign.views.SMSView#clearPlayerForView(org.bukkit.entity.Player) */ @Override public void clearPlayerForView(Player player) { super.clearPlayerForView(player); playerScrollPos.remove(getPlayerContext(player)); } /** * Get the suggested line length in characters. Default is 0 - subclasses should override this * as appropriate. Line length of 0 will disable any splitting. * * @return the suggested line length */ protected int getLineLength() { return 0; } /** * Get the desired maximum number of title lines for this view. * * @return the maximum number of title lines to draw */ protected int getMaxTitleLines() { int max = (Integer) getAttribute(MAX_TITLE_LINES); return max > 0 ? max : ScrollingMenuSign.getInstance().getConfig().getInt("sms.max_title_lines", 1); } /** * Get the hard maximum on the number of title lines this view supports. Override in subclasses. * * @return the hard limit for the maximum number of title lines */ protected int getHardMaxTitleLines() { return 1; } /** * Split the menu's title in the view's maximum line count, based on the view's suggested line length. * * @return a String list containing the split title */ public List<String> splitTitle(Player player) { String title = doVariableSubstitutions(player, getActiveMenuTitle(player)); int lineLength = getLineLength(); List<String> result = new ArrayList<String>(); int maxLines = Math.min(getMaxTitleLines(), getHardMaxTitleLines()); if (lineLength == 0 || maxLines == 1) { result.add(title); return result; } Scanner scanner = new Scanner(title); StringBuilder sb = new StringBuilder(title.length()); MarkupTracker markup = new MarkupTracker(); while (scanner.hasNext()) { String word = scanner.next(); markup.update(MarkupTracker.findMarkup(word)); // Debugger.getInstance().debug(2, getName() + ": buflen = " + sb.length() + " wordlen = " + word.length() + " line length = " + lineLength); if (sb.length() + word.length() + 1 <= lineLength || result.size() >= maxLines - 1) { // continue appending if (sb.length() > 0) sb.append(" "); sb.append(word); } else { // start a new line result.add(sb.toString()); sb = new StringBuilder(markup + word); lineLength = getLineLength() - markup.toString().length(); } } scanner.close(); result.add(sb.toString()); return result; } /* (non-Javadoc) * @see me.desht.scrollingmenusign.views.SMSView#onConfigurationChanged(me.desht.dhutils.ConfigurationManager, java.lang.String, java.lang.Object, java.lang.Object) */ @Override public void onConfigurationChanged(ConfigurationManager configurationManager, String key, Object oldVal, Object newVal) { super.onConfigurationChanged(configurationManager, key, oldVal, newVal); if (key.equals(MAX_TITLE_LINES)) { setDirty(true); } } @Override public Object onConfigurationValidate(ConfigurationManager configurationManager, String key, Object oldVal, Object newVal) { newVal = super.onConfigurationValidate(configurationManager, key, oldVal, newVal); if (key.equals(MAX_TITLE_LINES)) { if ((Integer) newVal > getHardMaxTitleLines() || (Integer) newVal < 0) { throw new SMSException("Valid " + MAX_TITLE_LINES + " range for this view is 0-" + getHardMaxTitleLines() + "."); } } return newVal; } private static class MarkupTracker { // null indicates value never set, 0 indicates a reset (&R) private Character colour = null; private Character text = null; public void update(MarkupTracker other) { if (other.colour != null) this.colour = other.colour; if (other.text != null) this.text = other.text; } @Override public String toString() { String s = ""; if (colour != null && colour != 0) s += "\u00a7" + colour; if (text != null && text != 0) s += "\u00a7" + text; return s; } public static MarkupTracker findMarkup(String s) { MarkupTracker m = new MarkupTracker(); for (int i = 0; i < s.length() - 1; i++) { if (s.charAt(i) == 0x00a7) { char c = Character.toUpperCase(s.charAt(i + 1)); if (c >= '0' && c <= '9' || c >= 'A' && c <= 'F') { m.colour = c; } else if (c == 'R') { m.text = m.colour = 0; } else { m.text = c; } } } return m; } } private class ScrollPosStack { private final Map<UUID, Deque<Integer>> stacks = new HashMap<UUID, Deque<Integer>>(); private void verify(Player player) { if (!stacks.containsKey(player.getUniqueId())) { stacks.put(player.getUniqueId(), new ArrayDeque<Integer>()); } } public void pushScrollPos(Player player, int pos) { verify(player); stacks.get(player.getUniqueId()).push(pos); } public int popScrollPos(Player player) { verify(player); Deque<Integer> stack = stacks.get(player.getUniqueId()); return stack.isEmpty() ? 1 : stack.pop(); } } }