content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.text.method;
  
  import android.text.Layout;
  import android.text.Spannable;
  import android.view.InputDevice;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.widget.TextView;
  
  /**
   * Base classes for movement methods.
   */
  public class BaseMovementMethod implements MovementMethod {
      @Override
      public boolean canSelectArbitrarily() {
          return false;
      }
  
      @Override
      public void initialize(TextView widget, Spannable text) {
      }
  
      @Override
      public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
          final int movementMetaState = getMovementMetaState(text, event);
          boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event);
          if (handled) {
              MetaKeyKeyListener.adjustMetaAfterKeypress(text);
              MetaKeyKeyListener.resetLockedMeta(text);
          }
          return handled;
      }
  
      @Override
      public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) {
          final int movementMetaState = getMovementMetaState(text, event);
          final int keyCode = event.getKeyCode();
          if (keyCode != KeyEvent.KEYCODE_UNKNOWN
                  && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
              final int repeat = event.getRepeatCount();
              boolean handled = false;
              for (int i = 0; i < repeat; i++) {
                  if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) {
                      break;
                  }
                  handled = true;
              }
              if (handled) {
                  MetaKeyKeyListener.adjustMetaAfterKeypress(text);
                  MetaKeyKeyListener.resetLockedMeta(text);
              }
              return handled;
          }
          return false;
      }
  
      @Override
      public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
          return false;
      }
  
      @Override
      public void onTakeFocus(TextView widget, Spannable text, int direction) {
      }
  
      @Override
      public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
          return false;
      }
  
      @Override
      public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
          return false;
      }
  
      @Override
      public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
          if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
              switch (event.getAction()) {
                  case MotionEvent.ACTION_SCROLL: {
                      final float vscroll;
                      final float hscroll;
                      if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
                          vscroll = 0;
                          hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
                      } else {
                          vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
                          hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
                      }
  
                      boolean handled = false;
                      if (hscroll < 0) {
                          handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll));
                      } else if (hscroll > 0) {
                          handled |= scrollRight(widget, text, (int)Math.ceil(hscroll));
                      }
                      if (vscroll < 0) {
                          handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll));
                      } else if (vscroll > 0) {
                          handled |= scrollDown(widget, text, (int)Math.ceil(vscroll));
                      }
                      return handled;
                  }
              }
          }
          return false;
      }
  
      /**
       * Gets the meta state used for movement using the modifiers tracked by the text
       * buffer as well as those present in the key event.
       *
       * The movement meta state excludes the state of locked modifiers or the SHIFT key
       * since they are not used by movement actions (but they may be used for selection).
       *
       * @param buffer The text buffer.
       * @param event The key event.
       * @return The keyboard meta states used for movement.
       */
      protected int getMovementMetaState(Spannable buffer, KeyEvent event) {
          // We ignore locked modifiers and SHIFT.
          int metaState = MetaKeyKeyListener.getMetaState(buffer, event)
                  & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED);
          return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK;
      }
  
      /**
       * Performs a movement key action.
       * The default implementation decodes the key down and invokes movement actions
       * such as {@link #down} and {@link #up}.
       * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once
       * to handle an {@link KeyEvent#ACTION_DOWN}.
       * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly
       * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @param event The key event.
       * @param keyCode The key code.
       * @param movementMetaState The keyboard meta states used for movement.
       * @param event The key event.
       * @return True if the event was handled.
       */
      protected boolean handleMovementKey(TextView widget, Spannable buffer,
              int keyCode, int movementMetaState, KeyEvent event) {
          switch (keyCode) {
              case KeyEvent.KEYCODE_DPAD_LEFT:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return left(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_CTRL_ON)) {
                      return leftWord(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_ALT_ON)) {
                      return lineStart(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_DPAD_RIGHT:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return right(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_CTRL_ON)) {
                      return rightWord(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_ALT_ON)) {
                      return lineEnd(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_DPAD_UP:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return up(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_ALT_ON)) {
                      return top(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_DPAD_DOWN:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return down(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_ALT_ON)) {
                      return bottom(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_PAGE_UP:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return pageUp(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_ALT_ON)) {
                      return top(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_PAGE_DOWN:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return pageDown(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_ALT_ON)) {
                      return bottom(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_MOVE_HOME:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return home(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_CTRL_ON)) {
                      return top(widget, buffer);
                  }
                  break;
  
              case KeyEvent.KEYCODE_MOVE_END:
                  if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                      return end(widget, buffer);
                  } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
                          KeyEvent.META_CTRL_ON)) {
                      return bottom(widget, buffer);
                  }
                  break;
          }
          return false;
      }
  
      /**
       * Performs a left movement action.
       * Moves the cursor or scrolls left by one character.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean left(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a right movement action.
       * Moves the cursor or scrolls right by one character.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean right(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs an up movement action.
       * Moves the cursor or scrolls up by one line.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean up(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a down movement action.
       * Moves the cursor or scrolls down by one line.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean down(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a page-up movement action.
       * Moves the cursor or scrolls up by one page.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean pageUp(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a page-down movement action.
       * Moves the cursor or scrolls down by one page.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean pageDown(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a top movement action.
       * Moves the cursor or scrolls to the top of the buffer.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean top(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a bottom movement action.
       * Moves the cursor or scrolls to the bottom of the buffer.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean bottom(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a line-start movement action.
       * Moves the cursor or scrolls to the start of the line.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean lineStart(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a line-end movement action.
       * Moves the cursor or scrolls to the end of the line.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean lineEnd(TextView widget, Spannable buffer) {
          return false;
      }
  
      /** {@hide} */
      protected boolean leftWord(TextView widget, Spannable buffer) {
          return false;
      }
  
      /** {@hide} */
      protected boolean rightWord(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs a home movement action.
       * Moves the cursor or scrolls to the start of the line or to the top of the
       * document depending on whether the insertion point is being moved or
       * the document is being scrolled.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean home(TextView widget, Spannable buffer) {
          return false;
      }
  
      /**
       * Performs an end movement action.
       * Moves the cursor or scrolls to the start of the line or to the top of the
       * document depending on whether the insertion point is being moved or
       * the document is being scrolled.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       */
      protected boolean end(TextView widget, Spannable buffer) {
          return false;
      }
  
      private int getTopLine(TextView widget) {
          return widget.getLayout().getLineForVertical(widget.getScrollY());
      }
  
      private int getBottomLine(TextView widget) {
          return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
      }
  
      private int getInnerWidth(TextView widget) {
          return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
      }
  
      private int getInnerHeight(TextView widget) {
          return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
      }
  
      private int getCharacterWidth(TextView widget) {
          return (int) Math.ceil(widget.getPaint().getFontSpacing());
      }
  
      private int getScrollBoundsLeft(TextView widget) {
          final Layout layout = widget.getLayout();
          final int topLine = getTopLine(widget);
          final int bottomLine = getBottomLine(widget);
          if (topLine > bottomLine) {
              return 0;
          }
          int left = Integer.MAX_VALUE;
          for (int line = topLine; line <= bottomLine; line++) {
              final int lineLeft = (int) Math.floor(layout.getLineLeft(line));
              if (lineLeft < left) {
                  left = lineLeft;
              }
          }
          return left;
      }
  
      private int getScrollBoundsRight(TextView widget) {
          final Layout layout = widget.getLayout();
          final int topLine = getTopLine(widget);
          final int bottomLine = getBottomLine(widget);
          if (topLine > bottomLine) {
              return 0;
          }
          int right = Integer.MIN_VALUE;
          for (int line = topLine; line <= bottomLine; line++) {
              final int lineRight = (int) Math.ceil(layout.getLineRight(line));
              if (lineRight > right) {
                  right = lineRight;
              }
          }
          return right;
      }
  
      /**
       * Performs a scroll left action.
       * Scrolls left by the specified number of characters.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @param amount The number of characters to scroll by.  Must be at least 1.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) {
          final int minScrollX = getScrollBoundsLeft(widget);
          int scrollX = widget.getScrollX();
          if (scrollX > minScrollX) {
              scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX);
              widget.scrollTo(scrollX, widget.getScrollY());
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll right action.
       * Scrolls right by the specified number of characters.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @param amount The number of characters to scroll by.  Must be at least 1.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollRight(TextView widget, Spannable buffer, int amount) {
          final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
          int scrollX = widget.getScrollX();
          if (scrollX < maxScrollX) {
              scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX);
              widget.scrollTo(scrollX, widget.getScrollY());
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll up action.
       * Scrolls up by the specified number of lines.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @param amount The number of lines to scroll by.  Must be at least 1.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollUp(TextView widget, Spannable buffer, int amount) {
          final Layout layout = widget.getLayout();
          final int top = widget.getScrollY();
          int topLine = layout.getLineForVertical(top);
          if (layout.getLineTop(topLine) == top) {
              // If the top line is partially visible, bring it all the way
              // into view; otherwise, bring the previous line into view.
              topLine -= 1;
          }
          if (topLine >= 0) {
              topLine = Math.max(topLine - amount + 1, 0);
              Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll down action.
       * Scrolls down by the specified number of lines.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @param amount The number of lines to scroll by.  Must be at least 1.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollDown(TextView widget, Spannable buffer, int amount) {
          final Layout layout = widget.getLayout();
          final int innerHeight = getInnerHeight(widget);
          final int bottom = widget.getScrollY() + innerHeight;
          int bottomLine = layout.getLineForVertical(bottom);
          if (layout.getLineTop(bottomLine + 1) < bottom + 1) {
              // Less than a pixel of this line is out of view,
              // so we must have tried to make it entirely in view
              // and now want the next line to be in view instead.
              bottomLine += 1;
          }
          final int limit = layout.getLineCount() - 1;
          if (bottomLine <= limit) {
              bottomLine = Math.min(bottomLine + amount - 1, limit);
              Touch.scrollTo(widget, layout, widget.getScrollX(),
                      layout.getLineTop(bottomLine + 1) - innerHeight);
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll page up action.
       * Scrolls up by one page.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollPageUp(TextView widget, Spannable buffer) {
          final Layout layout = widget.getLayout();
          final int top = widget.getScrollY() - getInnerHeight(widget);
          int topLine = layout.getLineForVertical(top);
          if (topLine >= 0) {
              Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll page up action.
       * Scrolls down by one page.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollPageDown(TextView widget, Spannable buffer) {
          final Layout layout = widget.getLayout();
          final int innerHeight = getInnerHeight(widget);
          final int bottom = widget.getScrollY() + innerHeight + innerHeight;
          int bottomLine = layout.getLineForVertical(bottom);
          if (bottomLine <= layout.getLineCount() - 1) {
              Touch.scrollTo(widget, layout, widget.getScrollX(),
                      layout.getLineTop(bottomLine + 1) - innerHeight);
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll to top action.
       * Scrolls to the top of the document.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollTop(TextView widget, Spannable buffer) {
          final Layout layout = widget.getLayout();
          if (getTopLine(widget) >= 0) {
              Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0));
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll to bottom action.
       * Scrolls to the bottom of the document.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollBottom(TextView widget, Spannable buffer) {
          final Layout layout = widget.getLayout();
          final int lineCount = layout.getLineCount();
          if (getBottomLine(widget) <= lineCount - 1) {
              Touch.scrollTo(widget, layout, widget.getScrollX(),
                      layout.getLineTop(lineCount) - getInnerHeight(widget));
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll to line start action.
       * Scrolls to the start of the line.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollLineStart(TextView widget, Spannable buffer) {
          final int minScrollX = getScrollBoundsLeft(widget);
          int scrollX = widget.getScrollX();
          if (scrollX > minScrollX) {
              widget.scrollTo(minScrollX, widget.getScrollY());
              return true;
          }
          return false;
      }
  
      /**
       * Performs a scroll to line end action.
       * Scrolls to the end of the line.
       *
       * @param widget The text view.
       * @param buffer The text buffer.
       * @return True if the event was handled.
       * @hide
       */
      protected boolean scrollLineEnd(TextView widget, Spannable buffer) {
          final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
          int scrollX = widget.getScrollX();
          if (scrollX < maxScrollX) {
              widget.scrollTo(maxScrollX, widget.getScrollY());
              return true;
          }
          return false;
      }
  }
  