gt:                     char c = Character.toUpperCase(s.charAt(i + 1));
context: 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) {
  