コード例 #1
0
ファイル: ListArea.java プロジェクト: luwrain/luwrain
 public static class Params {
   public ControlEnvironment environment;
   public Model model;
   public Appearance appearance;
   public Transition transition = new ListUtils.DefaultTransition();
   public ListClickHandler clickHandler;
   public String name;
   public Set<Flags> flags = EnumSet.of(Flags.EMPTY_LINE_BOTTOM);
 }
コード例 #2
0
ファイル: ListArea.java プロジェクト: luwrain/luwrain
 protected boolean onAnnounce() {
   environment.playSound(Sounds.INTRO_REGULAR);
   String item = "";
   if (selected() != null)
     item =
         appearance.getScreenAppearance(selected(), EnumSet.noneOf(Appearance.Flags.class)).trim();
   if (!item.isEmpty()) item = " " + item;
   environment.say(getAreaName() + item);
   return true;
 }
コード例 #3
0
ファイル: ListArea.java プロジェクト: luwrain/luwrain
public class ListArea implements Area, RegionProvider {
  public enum Flags {
    EMPTY_LINE_TOP,
    EMPTY_LINE_BOTTOM
  };

  protected static final Set<Appearance.Flags> NONE_APPEARANCE_FLAGS =
      EnumSet.noneOf(Appearance.Flags.class);
  protected static final Set<Appearance.Flags> BRIEF_ANNOUNCEMENT_ONLY =
      EnumSet.of(Appearance.Flags.BRIEF);

  public interface Model {
    int getItemCount();

    Object getItem(int index);

    boolean toggleMark(int index);

    void refresh();
  }

  public interface Appearance {
    public enum Flags {
      BRIEF
    };

    void announceItem(Object item, Set<Flags> flags);

    String getScreenAppearance(Object item, Set<Flags> flags);

    int getObservableLeftBound(Object item);

    int getObservableRightBound(Object item);
  }

  public interface Transition {
    public enum Type {
      SINGLE_DOWN,
      SINGLE_UP,
      PAGE_DOWN,
      PAGE_UP,
      HOME,
      END
    };

    public static class State {
      public enum Type {
        EMPTY_LINE_TOP,
        EMPTY_LINE_BOTTOM,
        ITEM_INDEX,
        NO_TRANSITION
      };

      public final Type type;
      public final int itemIndex;

      public State(Type type) {
        NullCheck.notNull(type, "type");
        this.type = type;
        this.itemIndex = -1;
      }

      public State(int itemIndex) {
        this.type = Type.ITEM_INDEX;
        this.itemIndex = itemIndex;
      }
    }

    State transition(
        Type type,
        State fromState,
        int itemCount,
        boolean hasEmptyLineTop,
        boolean hasEmptyLineBottom);
  }

  public static class Params {
    public ControlEnvironment environment;
    public Model model;
    public Appearance appearance;
    public Transition transition = new ListUtils.DefaultTransition();
    public ListClickHandler clickHandler;
    public String name;
    public Set<Flags> flags = EnumSet.of(Flags.EMPTY_LINE_BOTTOM);
  }

  protected final RegionTranslator region = new RegionTranslator(this);
  protected final ControlEnvironment environment;
  protected String areaName = "";
  protected final Model model;
  protected final Appearance appearance;
  protected final Transition transition;
  protected final Set<Flags> flags;
  protected ListClickHandler clickHandler;

  protected int hotPointX = 0;
  protected int hotPointY = 0;

  public ListArea(Params params) {
    NullCheck.notNull(params, "params");
    NullCheck.notNull(params.environment, "params.environment");
    NullCheck.notNull(params.model, "params.model");
    NullCheck.notNull(params.appearance, "params.appearance");
    NullCheck.notNull(params.transition, "params.transition");
    NullCheck.notNull(params.name, "params.name");
    NullCheck.notNull(params.flags, "params.flags");
    this.environment = params.environment;
    this.model = params.model;
    this.appearance = params.appearance;
    this.transition = params.transition;
    this.clickHandler = params.clickHandler;
    this.areaName = params.name;
    this.flags = params.flags;
    //	itemsLayout.setFlags(params.flags);
    resetHotPoint();
  }

  public void setListClickHandler(ListClickHandler clickHandler) {
    this.clickHandler = clickHandler;
  }

  public Model getListModel() {
    return model;
  }

  public Appearance getListAppearance() {
    return appearance;
  }

  /**
   * Returns the object in the model corresponding to current hot point position. If the model is
   * empty or hot point is on an empty line, this method always returns {@code null}.
   *
   * @return The object in the model associated with the currently selected line or {@code null} if
   *     there is no any
   */
  public final Object selected() {
    final int index = selectedIndex();
    return (index >= 0 && index < model.getItemCount()) ? model.getItem(index) : null;
  }

  /**
   * The index of the item in the model which is under the hot point in this list. This method
   * returns the index in the model, not on the screen. It means that the value returned by this
   * method may be different than the value returned by {@code getHotPointY()} (but may be equal as
   * well). If the list is empty or an empty line is selected, this method returns -1.
   *
   * @return The index of the selected line in the model or -1 if there is no any
   */
  public final int selectedIndex() {
    return getItemIndexOnLine(hotPointY);
  }

  /**
   * Searches for the item in the model and sets hot point on it. Given an arbitrary object, this
   * method looks through all items in the model and does a couple of checks: literal pointers
   * equality and a check with {@code equals()} method. If at least one of these checks succeeds,
   * the item is considered equal to the given one, and hot points is set on it.
   *
   * @param obj The object to search for
   * @param introduce Must be true if it is necessary to introduce the object, once it's found
   * @return True if the request object is found, false otherwise
   */
  public boolean select(Object obj, boolean introduce) {
    NullCheck.notNull(obj, "obj");
    for (int i = 0; i < model.getItemCount(); ++i) {
      final Object o = model.getItem(i);
      if (o == null || (obj != o && !obj.equals(o))) continue;
      hotPointY = i;
      hotPointX = appearance.getObservableLeftBound(o);
      environment.onAreaNewHotPoint(this);
      if (introduce) appearance.announceItem(o, NONE_APPEARANCE_FLAGS);
      return true;
    }
    return false;
  }

  /**
   * Selects the item by its index. Given the non-negative integer value as an index, this method
   * sets the hot point on the item addressed with this index, checking only that index is in
   * appropriate bounds. Index must address the object as a number in the model, ignoring any empty
   * lines.
   *
   * @param index The item index to select
   * @param announce Must be true, if it is necessary to announce the item , once it has been
   *     selected
   * @return True if the index is valid and the item gets hot point on it
   */
  public boolean select(int index, boolean announce) {
    if (index < 0 || index >= model.getItemCount()) return false;
    final int emptyCountAbove = flags.contains(Flags.EMPTY_LINE_TOP) ? 1 : 0;
    hotPointY = index + emptyCountAbove;
    final Object item = model.getItem(index);
    if (item != null) {
      hotPointX = appearance.getObservableLeftBound(item);
      if (announce) appearance.announceItem(item, NONE_APPEARANCE_FLAGS);
    } else {
      hotPointX = 0;
      if (announce) environment.hint(Hints.EMPTY_LINE);
    }
    environment.onAreaNewHotPoint(this);
    return true;
  }

  public int getItemIndexOnLine(int index) {
    final int linesTop = flags.contains(Flags.EMPTY_LINE_TOP) ? 1 : 0;
    if (index < linesTop) return -1;
    if (index - linesTop < model.getItemCount()) return index - linesTop;
    return -1;
  }

  public int getLineIndexByItemIndex(int index) {
    final int count = model.getItemCount();
    if (index < 0 || index >= count) return -1;
    final int linesTop = flags.contains(Flags.EMPTY_LINE_TOP) ? 1 : 0;
    return index + linesTop;
  }

  public void reset(boolean announce) {
    EnvironmentEvent.resetRegionPoint(this);
    resetHotPoint(announce);
  }

  public void resetHotPoint() {
    resetHotPoint(false);
  }

  public void resetHotPoint(boolean introduce) {
    hotPointY = 0;
    final int count = model.getItemCount();
    if (count < 1) {
      hotPointX = 0;
      environment.onAreaNewHotPoint(this);
      return;
    }
    final Object item = model.getItem(0);
    if (item != null) {
      hotPointX = item != null ? appearance.getObservableLeftBound(item) : 0;
      if (introduce) appearance.announceItem(item, NONE_APPEARANCE_FLAGS);
    } else {
      hotPointX = 0;
      environment.hint(Hints.EMPTY_LINE);
    }
    environment.onAreaNewHotPoint(this);
  }

  public void announceSelected() {
    final Object item = selected();
    if (item != null) appearance.announceItem(item, NONE_APPEARANCE_FLAGS);
  }

  /**
   * Refreshes the content of the list. This method calls {@code refresh()} method of the model and
   * displays new items. It does not produce any speech announcement of the change. HotPointY is
   * preserved if it is possible (meaning, the new number of lines not less than old value of
   * hotPointY), but hotPointX is moved to the beginning of the line.
   */
  public void refresh() {
    model.refresh();
    final int count = model.getItemCount();
    if (count == 0) {
      hotPointX = 0;
      hotPointY = 0;
      environment.onAreaNewContent(this);
      environment.onAreaNewHotPoint(this);
      return;
    }
    hotPointY = hotPointY < count ? hotPointY : count - 1;
    final Object item = model.getItem(hotPointY);
    if (item != null) hotPointX = appearance.getObservableLeftBound(item);
    else hotPointX = 0;
    environment.onAreaNewContent(this);
    environment.onAreaNewHotPoint(this);
  }

  public boolean isEmpty() {
    return model.getItemCount() <= 0;
  }

  @Override
  public boolean onKeyboardEvent(KeyboardEvent event) {
    NullCheck.notNull(event, "event");
    if (!event.isSpecial() && (!event.isModified() || event.withShiftOnly())) return onChar(event);
    if (!event.isSpecial() || event.isModified()) return false;
    switch (event.getSpecial()) {
      case ARROW_DOWN:
        return onArrowDown(event, false);
      case ARROW_UP:
        return onArrowUp(event, false);
      case ARROW_RIGHT:
        return onArrowRight(event);
      case ARROW_LEFT:
        return onArrowLeft(event);
      case ALTERNATIVE_ARROW_DOWN:
        return onArrowDown(event, true);
      case ALTERNATIVE_ARROW_UP:
        return onArrowUp(event, true);
      case ALTERNATIVE_ARROW_RIGHT:
        return onAltRight(event);
      case ALTERNATIVE_ARROW_LEFT:
        return onAltLeft(event);
      case HOME:
        return onHome(event);
      case END:
        return onEnd(event);
      case ALTERNATIVE_HOME:
        return onAltHome(event);
      case ALTERNATIVE_END:
        return onAltEnd(event);
      case PAGE_DOWN:
        return onPageDown(event, false);
      case PAGE_UP:
        return onPageUp(event, false);
      case ALTERNATIVE_PAGE_DOWN:
        return onPageDown(event, true);
      case ALTERNATIVE_PAGE_UP:
        return onPageUp(event, true);
      case INSERT:
        return onInsert(event);
      case ENTER:
        return onEnter(event);
      default:
        return false;
    }
  }

  @Override
  public boolean onEnvironmentEvent(EnvironmentEvent event) {
    NullCheck.notNull(event, "event");
    if (event.getType() != EnvironmentEvent.Type.REGULAR) return false;
    switch (event.getCode()) {
      case REFRESH:
        refresh();
        return true;
      case INTRODUCE:
        return onAnnounce();
      case ANNOUNCE_LINE:
        return onAnnounceLine();
      case OK:
        return onOk(event);
      case LISTENING_FINISHED:
        if (event instanceof ListeningFinishedEvent)
          return onListeningFinishedEvent((ListeningFinishedEvent) event);
        return false;
      case MOVE_HOT_POINT:
        if (event instanceof MoveHotPointEvent) return onMoveHotPoint((MoveHotPointEvent) event);
        return false;
      default:
        return region.onEnvironmentEvent(event, hotPointX, hotPointY);
    }
  }

  @Override
  public boolean onAreaQuery(AreaQuery query) {
    NullCheck.notNull(query, "query");
    switch (query.getQueryCode()) {
      case AreaQuery.BEGIN_LISTENING:
        if (query instanceof BeginListeningQuery)
          return onBeginListeningQuery((BeginListeningQuery) query);
        return false;
      default:
        return region.onAreaQuery(query, hotPointX, hotPointY);
    }
  }

  @Override
  public Action[] getAreaActions() {
    return new Action[0];
  }

  @Override
  public int getLineCount() {
    final int emptyCountTop = flags.contains(Flags.EMPTY_LINE_TOP) ? 1 : 0;
    final int emptyCountBottom = flags.contains(Flags.EMPTY_LINE_BOTTOM) ? 1 : 0;

    final int res = model.getItemCount() + emptyCountTop + emptyCountBottom;
    return res >= 1 ? res : 1;
  }

  @Override
  public String getLine(int index) {
    if (isEmpty()) return index == 0 ? noContentStr() : "";
    final int itemIndex = getItemIndexOnLine(index);
    if (itemIndex < 0 || itemIndex >= model.getItemCount()) return "";
    final Object res = model.getItem(itemIndex);
    return res != null ? appearance.getScreenAppearance(res, NONE_APPEARANCE_FLAGS) : "";
  }

  @Override
  public int getHotPointX() {
    return hotPointX >= 0 ? hotPointX : 0;
  }

  @Override
  public int getHotPointY() {
    return hotPointY >= 0 ? hotPointY : 0;
  }

  @Override
  public String getAreaName() {
    NullCheck.notNull(areaName, "areaName");
    return areaName;
  }

  public void setAreaName(String areaName) {
    NullCheck.notNull(areaName, "areaName");
    this.areaName = areaName;
    environment.onAreaNewName(this);
  }

  protected boolean onAnnounce() {
    environment.playSound(Sounds.INTRO_REGULAR);
    String item = "";
    if (selected() != null)
      item =
          appearance.getScreenAppearance(selected(), EnumSet.noneOf(Appearance.Flags.class)).trim();
    if (!item.isEmpty()) item = " " + item;
    environment.say(getAreaName() + item);
    return true;
  }

  protected boolean onAnnounceLine() {
    if (isEmpty()) return false;
    final Object item = selected();
    if (item == null) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    appearance.announceItem(item, NONE_APPEARANCE_FLAGS);
    return true;
  }

  protected boolean onMoveHotPoint(MoveHotPointEvent event) {
    NullCheck.notNull(event, "event");
    final int x = event.getNewHotPointX();
    final int y = event.getNewHotPointY();
    final int newY;
    if (y >= getLineCount()) {
      if (event.precisely()) return false;
      newY = getLineCount() - 1;
    } else newY = y;
    if (getItemIndexOnLine(newY) >= 0) {
      // Line with item, not empty
      final Object item = model.getItem(getItemIndexOnLine(newY));
      final int leftBound = appearance.getObservableLeftBound(item);
      final int rightBound = appearance.getObservableRightBound(item);
      if (event.precisely() && (x < leftBound || x > rightBound)) return false;
      hotPointY = newY;
      hotPointX = x;
      if (hotPointX < leftBound) hotPointX = leftBound;
      if (hotPointX > rightBound) hotPointX = rightBound;
      environment.onAreaNewHotPoint(this);
      return true;
    }
    // On empty line
    hotPointY = newY;
    hotPointX = 0;
    environment.onAreaNewHotPoint(this);
    return true;
  }

  protected boolean onBeginListeningQuery(BeginListeningQuery query) {
    NullCheck.notNull(query, "query");
    final int index = selectedIndex();
    if (index < 0) return false;
    final int count = model.getItemCount();
    if (index >= count) return false;
    final Object current = model.getItem(index);
    final String text =
        appearance
            .getScreenAppearance(current, NONE_APPEARANCE_FLAGS)
            .substring(hotPointX, appearance.getObservableRightBound(current));
    //	Log.debug("listen", appearance.getScreenAppearance(current, NONE_APPEARANCE_FLAGS));
    //	Log.debug("listen", "" + hotPointX);
    if (text.isEmpty() && index + 1 >= count) return false;
    if (index + 1 < count) {
      final Object next = model.getItem(index + 1);
      query.answer(
          new BeginListeningQuery.Answer(
              text, new ListeningInfo(index + 1, appearance.getObservableLeftBound(next))));
    } else
      query.answer(
          new BeginListeningQuery.Answer(
              text, new ListeningInfo(index, appearance.getObservableRightBound(current))));
    return true;
  }

  protected boolean onListeningFinishedEvent(ListeningFinishedEvent event) {
    NullCheck.notNull(event, "event");
    if (!(event.getExtraInfo() instanceof ListeningInfo)) return false;
    final ListeningInfo info = (ListeningInfo) event.getExtraInfo();
    final int count = model.getItemCount();
    if (info.itemIndex >= count) return false;
    final Object item = model.getItem(info.itemIndex);
    final int leftBound = appearance.getObservableLeftBound(item);
    final int rightBound = appearance.getObservableRightBound(item);
    if (info.pos < leftBound || info.pos > rightBound) return false;
    hotPointY = getLineIndexByItemIndex(info.itemIndex);
    hotPointX = info.pos;
    environment.onAreaNewHotPoint(this);
    return true;
  }

  protected boolean onChar(KeyboardEvent event) {
    if (noContent()) return true;
    final int count = model.getItemCount();
    final char c = event.getChar();
    final String beginning;
    if (selected() != null) {
      if (hotPointX >= appearance.getObservableRightBound(selected())) return false;
      final String name = getObservableSubstr(selected());
      final int pos =
          Math.min(hotPointX - appearance.getObservableLeftBound(selected()), name.length());
      if (pos < 0) return false;
      beginning = name.substring(0, pos);
    } else beginning = "";
    Log.debug("list", "beginning:" + beginning);
    final String mustBegin = beginning + c;
    for (int i = 0; i < count; ++i) {
      Log.debug("list", "checking:" + i);
      final String name = getObservableSubstr(model.getItem(i));
      Log.debug("list", "name:" + name);
      if (!name.startsWith(mustBegin)) continue;
      hotPointY = getLineIndexByItemIndex(i);
      Log.debug("list", "hotPointY:" + hotPointY);
      ++hotPointX;
      appearance.announceItem(model.getItem(hotPointY), NONE_APPEARANCE_FLAGS);
      environment.onAreaNewHotPoint(this);
      return true;
    }
    return false;
  }

  protected boolean onArrowDown(KeyboardEvent event, boolean briefAnnouncement) {
    return onTransition(Transition.Type.SINGLE_DOWN, Hints.NO_ITEMS_BELOW, briefAnnouncement);
  }

  protected boolean onArrowUp(KeyboardEvent event, boolean briefAnnouncement) {
    return onTransition(Transition.Type.SINGLE_UP, Hints.NO_ITEMS_ABOVE, briefAnnouncement);
  }

  protected boolean onPageDown(KeyboardEvent event, boolean briefAnnouncement) {
    return onTransition(Transition.Type.PAGE_DOWN, Hints.NO_ITEMS_BELOW, briefAnnouncement);
  }

  protected boolean onPageUp(KeyboardEvent event, boolean briefAnnouncement) {
    return onTransition(Transition.Type.PAGE_UP, Hints.NO_ITEMS_ABOVE, briefAnnouncement);
  }

  protected boolean onEnd(KeyboardEvent event) {
    return onTransition(Transition.Type.END, Hints.NO_ITEMS_BELOW, false);
  }

  protected boolean onHome(KeyboardEvent event) {
    return onTransition(Transition.Type.HOME, Hints.NO_ITEMS_ABOVE, false);
  }

  protected boolean onTransition(Transition.Type type, int hint, boolean briefAnnouncement) {
    NullCheck.notNull(type, "type");
    //	NullCheck.notNull(hint, "hint");
    if (noContent()) return true;
    final int index = selectedIndex();
    final int count = model.getItemCount();
    final int emptyCountTop = flags.contains(Flags.EMPTY_LINE_TOP) ? 1 : 0;
    final Transition.State current;
    if (index >= 0) current = new Transition.State(index);
    else if (flags.contains(Flags.EMPTY_LINE_TOP) && hotPointY == 0)
      current = new Transition.State(Transition.State.Type.EMPTY_LINE_TOP);
    else if (flags.contains(Flags.EMPTY_LINE_BOTTOM) && hotPointY == count + emptyCountTop)
      current = new Transition.State(Transition.State.Type.EMPTY_LINE_BOTTOM);
    else return false;
    final Transition.State newState =
        transition.transition(
            type,
            current,
            count,
            flags.contains(Flags.EMPTY_LINE_TOP),
            flags.contains(Flags.EMPTY_LINE_BOTTOM));
    NullCheck.notNull(newState, "newState");
    Log.debug("list", "newState=" + newState.type);
    switch (newState.type) {
      case NO_TRANSITION:
        environment.hint(hint);
        return true;
      case EMPTY_LINE_TOP:
        if (!flags.contains(Flags.EMPTY_LINE_TOP)) return false;
        hotPointY = 0;
        break;
      case EMPTY_LINE_BOTTOM:
        if (!flags.contains(Flags.EMPTY_LINE_BOTTOM)) return false;
        hotPointY = count + emptyCountTop;
        break;
      case ITEM_INDEX:
        if (newState.itemIndex < 0 || newState.itemIndex >= count) return false;
        hotPointY = newState.itemIndex + emptyCountTop;
        break;
      default:
        return false;
    }
    onNewHotPointY(briefAnnouncement);
    return true;
  }

  protected boolean onArrowRight(KeyboardEvent event) {
    if (noContent()) return true;
    final Object item = selected();
    NullCheck.notNull(item, "item");
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    if (line.isEmpty()) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final int rightBound = appearance.getObservableRightBound(item);
    if (hotPointX >= rightBound) {
      environment.hint(Hints.END_OF_LINE);
      return true;
    }
    ++hotPointX;
    announceChar(line, hotPointX, rightBound);
    environment.onAreaNewHotPoint(this);
    return true;
  }

  protected boolean onArrowLeft(KeyboardEvent event) {
    if (noContent()) return true;
    final Object item = selected();
    NullCheck.notNull(item, "item");
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    if (line.isEmpty()) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final int leftBound = appearance.getObservableLeftBound(item);
    final int rightBound = appearance.getObservableRightBound(item);
    if (hotPointX <= leftBound) {
      environment.hint(Hints.BEGIN_OF_LINE);
      return true;
    }
    --hotPointX;
    announceChar(line, hotPointX, rightBound);
    environment.onAreaNewHotPoint(this);
    return true;
  }

  private boolean onAltRight(KeyboardEvent event) {
    if (noContent()) return true;
    final Object item = selected();
    if (item == null) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    if (line.isEmpty()) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final int leftBound = appearance.getObservableLeftBound(item);
    final int rightBound = appearance.getObservableRightBound(item);
    if (hotPointX >= rightBound) {
      environment.hint(Hints.END_OF_LINE);
      return true;
    }
    final String subline = line.substring(leftBound, rightBound);
    final WordIterator it = new WordIterator(subline, hotPointX - leftBound);
    if (!it.stepForward()) {
      environment.hint(Hints.END_OF_LINE);
      return true;
    }
    hotPointX = it.pos() + leftBound;
    if (it.announce().length() > 0) environment.say(it.announce());
    else environment.hint(Hints.END_OF_LINE);
    environment.onAreaNewHotPoint(this);
    return true;
  }

  private boolean onAltLeft(KeyboardEvent event) {
    if (noContent()) return true;
    final Object item = selected();
    if (item == null) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    if (line.isEmpty()) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final int leftBound = appearance.getObservableLeftBound(item);
    final int rightBound = appearance.getObservableRightBound(item);
    if (hotPointX <= leftBound) {
      environment.hint(Hints.BEGIN_OF_LINE);
      return true;
    }
    final String subline = line.substring(leftBound, rightBound);
    final WordIterator it = new WordIterator(subline, hotPointX - leftBound);
    if (!it.stepBackward()) {
      environment.hint(Hints.BEGIN_OF_LINE);
      return true;
    }
    hotPointX = it.pos() + leftBound;
    environment.say(it.announce());
    environment.onAreaNewHotPoint(this);
    return true;
  }

  private boolean onAltEnd(KeyboardEvent event) {
    if (noContent()) return true;
    final Object item = selected();
    if (item == null) {
      environment.hint(Hints.EMPTY_LINE);
      return true;
    }
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    hotPointX = appearance.getObservableRightBound(item);
    environment.hint(Hints.END_OF_LINE);
    environment.onAreaNewHotPoint(this);
    return true;
  }

  private boolean onAltHome(KeyboardEvent event) {
    if (noContent()) return true;
    final Object item = selected();
    NullCheck.notNull(item, "item");
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    hotPointX = appearance.getObservableLeftBound(item);
    announceChar(line, hotPointX, appearance.getObservableRightBound(item));
    environment.onAreaNewHotPoint(this);
    return true;
  }

  protected boolean onInsert(KeyboardEvent event) {
    final int index = selectedIndex();
    if (index < 0) return false;
    if (!model.toggleMark(index)) return false;
    environment.onAreaNewContent(this);
    if (hotPointY + 1 < getLineCount()) {
      ++hotPointY;
      onNewHotPointY(false);
    }
    return true;
  }

  protected boolean onEnter(KeyboardEvent event) {
    if (isEmpty() || clickHandler == null) return false;
    if (selected() == null || selectedIndex() < 0) return false;
    return clickHandler.onListClick(this, selectedIndex(), selected());
  }

  private boolean onOk(EnvironmentEvent event) {
    if (clickHandler == null) return false;
    final int index = selectedIndex();
    final Object item = selected();
    if (index < 0 || item == null) return false;
    return clickHandler.onListClick(this, index, item);
  }

  @Override
  public RegionContent getWholeRegion() {
    if (model == null || model.getItemCount() < 0) return null;
    final LinkedList<String> res = new LinkedList<String>();
    final int count = model.getItemCount();
    for (int i = 0; i < count; ++i) {
      final String line = appearance.getScreenAppearance(model.getItem(i), NONE_APPEARANCE_FLAGS);
      res.add(line != null ? line : "");
    }
    res.add("");
    return new RegionContent(res.toArray(new String[res.size()]));
  }

  @Override
  public RegionContent getRegion(int fromX, int fromY, int toX, int toY) {
    if (model == null || model.getItemCount() < 0) return null;
    if (fromY >= model.getItemCount() || toY > model.getItemCount()) return null;
    if (fromY == toY) {
      final String line =
          appearance.getScreenAppearance(model.getItem(fromY), NONE_APPEARANCE_FLAGS);
      if (line == null || line.isEmpty()) return null;
      final int fromPos = fromX < line.length() ? fromX : line.length();
      final int toPos = toX < line.length() ? toX : line.length();
      if (fromPos >= toPos) return null;
      return new RegionContent(new String[] {line.substring(fromPos, toPos)});
    }
    final LinkedList<String> res = new LinkedList<String>();
    for (int i = fromY; i < toY; ++i) {
      final String line = appearance.getScreenAppearance(model.getItem(i), NONE_APPEARANCE_FLAGS);
      res.add(line != null ? line : "");
    }
    res.add("");
    return new RegionContent(res.toArray(new String[res.size()]));
  }

  @Override
  public boolean deleteWholeRegion() {
    return false;
  }

  @Override
  public boolean deleteRegion(int fromX, int fromY, int toX, int toY) {
    return false;
  }

  @Override
  public boolean insertRegion(int x, int y, RegionContent data) {
    return false;
  }

  protected void onNewHotPointY(boolean briefAnnouncement) {
    final int index = selectedIndex();
    if (index < 0) {
      environment.hint(Hints.EMPTY_LINE);
      hotPointX = 0;
      environment.onAreaNewHotPoint(this);
      return;
    }
    final Object item = model.getItem(index);
    if (item == null) {
      environment.hint(Hints.EMPTY_LINE);
      hotPointX = 0;
      environment.onAreaNewHotPoint(this);
      return;
    }
    appearance.announceItem(
        item, briefAnnouncement ? BRIEF_ANNOUNCEMENT_ONLY : NONE_APPEARANCE_FLAGS);
    hotPointX = appearance.getObservableLeftBound(item);
    environment.onAreaNewHotPoint(this);
  }

  protected String getObservableSubstr(Object item) {
    NullCheck.notNull(item, "item");
    final String line = appearance.getScreenAppearance(item, NONE_APPEARANCE_FLAGS);
    NullCheck.notNull(line, "line");
    if (line.isEmpty()) return "";
    final int leftBound = Math.min(appearance.getObservableLeftBound(item), line.length());
    final int rightBound = Math.min(appearance.getObservableRightBound(item), line.length());
    if (leftBound >= rightBound) return "";
    return line.substring(leftBound, rightBound);
  }

  protected String noContentStr() {
    return environment.getStaticStr("ListNoContent");
  }

  protected void announceChar(String line, int pos, int rightBound) {
    NullCheck.notNull(line, "line");
    if (pos < rightBound) environment.sayLetter(line.charAt(pos));
    else environment.hint(Hints.END_OF_LINE);
  }

  protected boolean noContent() {
    if (model == null || model.getItemCount() < 1) {
      environment.hint(noContentStr(), Hints.NO_CONTENT);
      return true;
    }
    return false;
  }

  protected static class ListeningInfo {
    final int itemIndex;
    final int pos;

    ListeningInfo(int itemIndex, int pos) {
      this.itemIndex = itemIndex;
      this.pos = pos;
    }
  }
}