/*
* Copyright (C) 2007 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 jackpal.androidterm.emulatorview;
import jackpal.androidterm.emulatorview.compat.ClipboardManagerCompat;
import jackpal.androidterm.emulatorview.compat.ClipboardManagerCompatFactory;
import java.io.IOException;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
/**
* A view on a {@link TermSession}. Displays the terminal emulator's screen,
* provides access to its scrollback buffer, and passes input through to the
* terminal emulator.
* <p>
* If this view is inflated from an XML layout, you need to call {@link
* #attachSession attachSession} and {@link #setDensity setDensity} before using
* the view. If creating this view from code, use the {@link
* #EmulatorView(Context, TermSession, DisplayMetrics)} constructor, which will
* take care of this for you.
*/
public class EmulatorView extends View implements GestureDetector.OnGestureListener {
private final static String TAG = "EmulatorView";
private final static boolean LOG_KEY_EVENTS = false;
private final static boolean LOG_IME = false;
/**
* We defer some initialization until we have been layed out in the view
* hierarchy. The boolean tracks when we know what our size is.
*/
private boolean mKnownSize;
// Set if initialization was deferred because a TermSession wasn't attached
private boolean mDeferInit = false;
private int mVisibleWidth;
private int mVisibleHeight;
private TermSession mTermSession;
/**
* Our transcript. Contains the screen and the transcript.
*/
private TranscriptScreen mTranscriptScreen;
/**
* Total width of each character, in pixels
*/
private float mCharacterWidth;
/**
* Total height of each character, in pixels
*/
private int mCharacterHeight;
/**
* Top-of-screen margin
*/
private int mTopOfScreenMargin;
/**
* Used to render text
*/
private TextRenderer mTextRenderer;
/**
* Text size. Zero means 4 x 8 font.
*/
private int mTextSize = 10;
private int mCursorStyle;
private int mCursorBlink;
/**
* Color scheme (default foreground/background colors).
*/
private ColorScheme mColorScheme = BaseTextRenderer.defaultColorScheme;
private Paint mForegroundPaint;
private Paint mBackgroundPaint;
private boolean mUseCookedIme;
/**
* Our terminal emulator. We use this to get the current cursor position.
*/
private TerminalEmulator mEmulator;
/**
* The number of rows of text to display.
*/
private int mRows;
/**
* The number of columns of text to display.
*/
private int mColumns;
/**
* The number of columns that are visible on the display.
*/
private int mVisibleColumns;
/**
* The top row of text to display. Ranges from -activeTranscriptRows to 0
*/
private int mTopRow;
private int mLeftColumn;
private static final int CURSOR_BLINK_PERIOD = 1000;
private boolean mCursorVisible = true;
private boolean mIsSelectingText = false;
private boolean mBackKeySendsCharacter = false;
private int mControlKeyCode;
private int mFnKeyCode;
private boolean mIsControlKeySent = false;
private boolean mIsFnKeySent = false;
private String mTermType;
private float mDensity;
private float mScaledDensity;
private static final int SELECT_TEXT_OFFSET_Y = -40;
private int mSelXAnchor = -1;
private int mSelYAnchor = -1;
private int mSelX1 = -1;
private int mSelY1 = -1;
private int mSelX2 = -1;
private int mSelY2 = -1;
/**
* Routing alt and meta keyCodes away from the IME allows Alt key processing to work on
* the Asus Transformer TF101.
* It doesn't seem to harm anything else, but it also doesn't seem to be
* required on other platforms.
*
* This test should be refined as we learn more.
*/
private final static boolean sTrapAltAndMeta = Build.MODEL.contains("Transformer TF101");
private Runnable mBlinkCursor = new Runnable() {
public void run() {
if (mCursorBlink != 0) {
mCursorVisible = ! mCursorVisible;
mHandler.postDelayed(this, CURSOR_BLINK_PERIOD);
} else {
mCursorVisible = true;
}
// Perhaps just invalidate the character with the cursor.
invalidate();
}
};
private GestureDetector mGestureDetector;
private GestureDetector.OnGestureListener mExtGestureListener;
private Scroller mScroller;
private Runnable mFlingRunner = new Runnable() {
public void run() {
if (mScroller.isFinished()) {
return;
}
boolean more = mScroller.computeScrollOffset();
int newTopRow = mScroller.getCurrY();
if (newTopRow != mTopRow) {
mTopRow = newTopRow;
invalidate();
}
if (more) {
post(this);
}
}
};
private float mScrollRemainder;
private TermKeyListener mKeyListener;
private String mImeBuffer = "";
/**
* Our message handler class. Implements a periodic callback.
*/
private final Handler mHandler = new Handler();
/**
* Called by the TermSession when the contents of the view need updating
*/
private UpdateCallback mUpdateNotify = new UpdateCallback() {
public void onUpdate() {
if ( mIsSelectingText ) {
int rowShift = mEmulator.getScrollCounter();
mSelY1 -= rowShift;
mSelY2 -= rowShift;
mSelYAnchor -= rowShift;
}
mEmulator.clearScrollCounter();
ensureCursorVisible();
invalidate();
}
};
/**
* Create an <code>EmulatorView</code> for a {@link TermSession}.
*
* @param context The {@link Context} for the view.
* @param session The {@link TermSession} this view will be displaying.
* @param metrics The {@link DisplayMetrics} of the screen on which the view
* will be displayed.
*/
public EmulatorView(Context context, TermSession session, DisplayMetrics metrics) {
super(context);
attachSession(session);
setDensity(metrics);
commonConstructor(context);
}
/**
* Constructor called when inflating this view from XML.
* <p>
* You should call {@link #attachSession attachSession} and {@link
* #setDensity setDensity} before using an <code>EmulatorView</code> created
* using this constructor.
*/
public EmulatorView(Context context, AttributeSet attrs) {
super(context, attrs);
commonConstructor(context);
}
/**
* Constructor called when inflating this view from XML with a
* default style set.
* <p>
* You should call {@link #attachSession attachSession} and {@link
* #setDensity setDensity} before using an <code>EmulatorView</code> created
* using this constructor.
*/
public EmulatorView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
commonConstructor(context);
}
private void commonConstructor(Context context) {
// TODO: See if we want to use the API level 11 constructor to get new flywheel feature.
mScroller = new Scroller(context);
}
/**
* Attach a {@link TermSession} to this view.
*
* @param session The {@link TermSession} this view will be displaying.
*/
public void attachSession(TermSession session) {
mTextRenderer = null;
mForegroundPaint = new Paint();
mBackgroundPaint = new Paint();
mTopRow = 0;
mLeftColumn = 0;
mGestureDetector = new GestureDetector(this);
// mGestureDetector.setIsLongpressEnabled(false);
setVerticalScrollBarEnabled(true);
setFocusable(true);
setFocusableInTouchMode(true);
mTermSession = session;
mKeyListener = new TermKeyListener(session);
// Do init now if it was deferred until a TermSession was attached
if (mDeferInit) {
mDeferInit = false;
mKnownSize = true;
initialize();
}
}
/**
* Update the screen density for the screen on which the view is displayed.
*
* @param metrics The {@link DisplayMetrics} of the screen.
*/
public void setDensity(DisplayMetrics metrics) {
if (mDensity == 0) {
// First time we've known the screen density, so update font size
mTextSize = (int) (mTextSize * metrics.density);
}
mDensity = metrics.density;
mScaledDensity = metrics.scaledDensity;
}
/**
* Inform the view that it is now visible on screen.
*/
public void onResume() {
updateSize(false);
if (mCursorBlink != 0) {
mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD);
}
mKeyListener.onResume();
}
/**
* Inform the view that it is no longer visible on the screen.
*/
public void onPause() {
if (mCursorBlink != 0) {
mHandler.removeCallbacks(mBlinkCursor);
}
mKeyListener.onPause();
}
/**
* Set this <code>EmulatorView</code>'s color scheme.
*
* @param scheme The {@link ColorScheme} to use (use null for the default
* scheme).
* @see TermSession#setColorScheme
* @see ColorScheme
*/
public void setColorScheme(ColorScheme scheme) {
if (scheme == null) {
mColorScheme = BaseTextRenderer.defaultColorScheme;
} else {
mColorScheme = scheme;
}
updateText();
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = mUseCookedIme ?
EditorInfo.TYPE_CLASS_TEXT :
EditorInfo.TYPE_NULL;
return new BaseInputConnection(this, true) {
/**
* Used to handle composing text requests
*/
private int mCursor;
private int mComposingTextStart;
private int mComposingTextEnd;
private int mSelectedTextStart;
private int mSelectedTextEnd;
private void sendText(CharSequence text) {
int n = text.length();
char c;
try {
for(int i = 0; i < n; i++) {
c = text.charAt(i);
if (Character.isHighSurrogate(c)) {
int codePoint;
if (++i < n) {
codePoint = Character.toCodePoint(c, text.charAt(i));
} else {
// Unicode Replacement Glyph, aka white question mark in black diamond.
codePoint = '\ufffd';
}
mapAndSend(codePoint);
} else {
mapAndSend(c);
}
}
} catch (IOException e) {
Log.e(TAG, "error writing ", e);
}
}
private void mapAndSend(int c) throws IOException {
int result = mKeyListener.mapControlChar(c);
if (result < TermKeyListener.KEYCODE_OFFSET) {
mTermSession.write(result);
} else {
mKeyListener.handleKeyCode(result - TermKeyListener.KEYCODE_OFFSET, getKeypadApplicationMode());
}
clearSpecialKeyStatus();
}
public boolean beginBatchEdit() {
if (LOG_IME) {
Log.w(TAG, "beginBatchEdit");
}
setImeBuffer("");
mCursor = 0;
mComposingTextStart = 0;
mComposingTextEnd = 0;
return true;
}
public boolean clearMetaKeyStates(int arg0) {
if (LOG_IME) {
Log.w(TAG, "clearMetaKeyStates " + arg0);
}
return false;
}
public boolean commitCompletion(CompletionInfo arg0) {
if (LOG_IME) {
Log.w(TAG, "commitCompletion " + arg0);
}
return false;
}
public boolean endBatchEdit() {
if (LOG_IME) {
Log.w(TAG, "endBatchEdit");
}
return true;
}
public boolean finishComposingText() {
if (LOG_IME) {
Log.w(TAG, "finishComposingText");
}
sendText(mImeBuffer);
setImeBuffer("");
mComposingTextStart = 0;
mComposingTextEnd = 0;
mCursor = 0;
return true;
}
public int getCursorCapsMode(int reqModes) {
if (LOG_IME) {
Log.w(TAG, "getCursorCapsMode(" + reqModes + ")");
}
int mode = 0;
if ((reqModes & TextUtils.CAP_MODE_CHARACTERS) != 0) {
mode |= TextUtils.CAP_MODE_CHARACTERS;
}
return mode;
}
public ExtractedText getExtractedText(ExtractedTextRequest arg0,
int arg1) {
if (LOG_IME) {
Log.w(TAG, "getExtractedText" + arg0 + "," + arg1);
}
return null;
}
public CharSequence getTextAfterCursor(int n, int flags) {
if (LOG_IME) {
Log.w(TAG, "getTextAfterCursor(" + n + "," + flags + ")");
}
int len = Math.min(n, mImeBuffer.length() - mCursor);
if (len <= 0 || mCursor < 0 || mCursor >= mImeBuffer.length()) {
return "";
}
return mImeBuffer.substring(mCursor, mCursor + len);
}
public CharSequence getTextBeforeCursor(int n, int flags) {
if (LOG_IME) {
Log.w(TAG, "getTextBeforeCursor(" + n + "," + flags + ")");
}
int len = Math.min(n, mCursor);
if (len <= 0 || mCursor < 0 || mCursor >= mImeBuffer.length()) {
return "";
}
return mImeBuffer.substring(mCursor-len, mCursor);
}
public boolean performContextMenuAction(int arg0) {
if (LOG_IME) {
Log.w(TAG, "performContextMenuAction" + arg0);
}
return true;
}
public boolean performPrivateCommand(String arg0, Bundle arg1) {
if (LOG_IME) {
Log.w(TAG, "performPrivateCommand" + arg0 + "," + arg1);
}
return true;
}
public boolean reportFullscreenMode(boolean arg0) {
if (LOG_IME) {
Log.w(TAG, "reportFullscreenMode" + arg0);
}
return true;
}
public boolean commitCorrection (CorrectionInfo correctionInfo) {
if (LOG_IME) {
Log.w(TAG, "commitCorrection");
}
return true;
}
public boolean commitText(CharSequence text, int newCursorPosition) {
if (LOG_IME) {
Log.w(TAG, "commitText(\"" + text + "\", " + newCursorPosition + ")");
}
clearComposingText();
sendText(text);
setImeBuffer("");
mCursor = 0;
return true;
}
private void clearComposingText() {
int len = mImeBuffer.length();
if (mComposingTextStart > len || mComposingTextEnd > len) {
mComposingTextEnd = mComposingTextStart = 0;
return;
}
setImeBuffer(mImeBuffer.substring(0, mComposingTextStart) +
mImeBuffer.substring(mComposingTextEnd));
if (mCursor < mComposingTextStart) {
// do nothing
} else if (mCursor < mComposingTextEnd) {
mCursor = mComposingTextStart;
} else {
mCursor -= mComposingTextEnd - mComposingTextStart;
}
mComposingTextEnd = mComposingTextStart = 0;
}
public boolean deleteSurroundingText(int leftLength, int rightLength) {
if (LOG_IME) {
Log.w(TAG, "deleteSurroundingText(" + leftLength +
"," + rightLength + ")");
}
if (leftLength > 0) {
for (int i = 0; i < leftLength; i++) {
sendKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
}
} else if ((leftLength == 0) && (rightLength == 0)) {
// Delete key held down / repeating
sendKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
}
// TODO: handle forward deletes.
return true;
}
public boolean performEditorAction(int actionCode) {
if (LOG_IME) {
Log.w(TAG, "performEditorAction(" + actionCode + ")");
}
if (actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) {
// The "return" key has been pressed on the IME.
sendText("\r");
}
return true;
}
public boolean sendKeyEvent(KeyEvent event) {
if (LOG_IME) {
Log.w(TAG, "sendKeyEvent(" + event + ")");
}
// Some keys are sent here rather than to commitText.
// In particular, del and the digit keys are sent here.
// (And I have reports that the HTC Magic also sends Return here.)
// As a bit of defensive programming, handle every key.
dispatchKeyEvent(event);
return true;
}
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (LOG_IME) {
Log.w(TAG, "setComposingText(\"" + text + "\", " + newCursorPosition + ")");
}
int len = mImeBuffer.length();
if (mComposingTextStart > len || mComposingTextEnd > len) {
return false;
}
setImeBuffer(mImeBuffer.substring(0, mComposingTextStart) +
text + mImeBuffer.substring(mComposingTextEnd));
mComposingTextEnd = mComposingTextStart + text.length();
mCursor = newCursorPosition > 0 ? mComposingTextEnd + newCursorPosition - 1
: mComposingTextStart - newCursorPosition;
return true;
}
public boolean setSelection(int start, int end) {
if (LOG_IME) {
Log.w(TAG, "setSelection" + start + "," + end);
}
int length = mImeBuffer.length();
if (start == end && start > 0 && start < length) {
mSelectedTextStart = mSelectedTextEnd = 0;
mCursor = start;
} else if (start < end && start > 0 && end < length) {
mSelectedTextStart = start;
mSelectedTextEnd = end;
mCursor = start;
}
return true;
}
public boolean setComposingRegion(int start, int end) {
if (LOG_IME) {
Log.w(TAG, "setComposingRegion " + start + "," + end);
}
if (start < end && start > 0 && end < mImeBuffer.length()) {
clearComposingText();
mComposingTextStart = start;
mComposingTextEnd = end;
}
return true;
}
public CharSequence getSelectedText(int flags) {
if (LOG_IME) {
Log.w(TAG, "getSelectedText " + flags);
}
int len = mImeBuffer.length();
if (mSelectedTextEnd >= len || mSelectedTextStart > mSelectedTextEnd) {
return "";
}
return mImeBuffer.substring(mSelectedTextStart, mSelectedTextEnd+1);
}
};
}
private void setImeBuffer(String buffer) {
if (!buffer.equals(mImeBuffer)) {
invalidate();
}
mImeBuffer = buffer;
}
/**
* Get the terminal emulator's keypad application mode.
*/
public boolean getKeypadApplicationMode() {
return mEmulator.getKeypadApplicationMode();
}
/**
* Set a {@link android.view.GestureDetector.OnGestureListener
* GestureDetector.OnGestureListener} to receive gestures performed on this
* view. Can be used to implement additional
* functionality via touch gestures or override built-in gestures.
*
* @param listener The {@link
* android.view.GestureDetector.OnGestureListener
* GestureDetector.OnGestureListener} which will receive
* gestures.
*/
public void setExtGestureListener(GestureDetector.OnGestureListener listener) {
mExtGestureListener = listener;
}
/**
* Compute the vertical range that the vertical scrollbar represents.
*/
@Override
protected int computeVerticalScrollRange() {
return mTranscriptScreen.getActiveRows();
}
/**
* Compute the vertical extent of the horizontal scrollbar's thumb within
* the vertical range. This value is used to compute the length of the thumb
* within the scrollbar's track.
*/
@Override
protected int computeVerticalScrollExtent() {
return mRows;
}
/**
* Compute the vertical offset of the vertical scrollbar's thumb within the
* horizontal range. This value is used to compute the position of the thumb
* within the scrollbar's track.
*/
@Override
protected int computeVerticalScrollOffset() {
return mTranscriptScreen.getActiveRows() + mTopRow - mRows;
}
/**
* Call this to initialize the view.
*/
private void initialize() {
TermSession session = mTermSession;
updateText();
mTranscriptScreen = session.getTranscriptScreen();
mEmulator = session.getEmulator();
session.setUpdateCallback(mUpdateNotify);
requestFocus();
}
/**
* Get the {@link TermSession} corresponding to this view.
*
* @return The {@link TermSession} object for this view.
*/
public TermSession getTermSession() {
return mTermSession;
}
/**
* Get the width of the visible portion of this view.
*
* @return The width of the visible portion of this view, in pixels.
*/
public int getVisibleWidth() {
return mVisibleWidth;
}
/**
* Get the height of the visible portion of this view.
*
* @return The height of the visible portion of this view, in pixels.
*/
public int getVisibleHeight() {
return mVisibleHeight;
}
/**
* Page the terminal view (scroll it up or down by <code>delta</code>
* screenfuls).
*
* @param delta The number of screens to scroll. Positive means scroll down,
* negative means scroll up.
*/
public void page(int delta) {
mTopRow =
Math.min(0, Math.max(-(mTranscriptScreen
.getActiveTranscriptRows()), mTopRow + mRows * delta));
invalidate();
}
/**
* Page the terminal view horizontally.
*
* @param deltaColumns the number of columns to scroll. Positive scrolls to
* the right.
*/
public void pageHorizontal(int deltaColumns) {
mLeftColumn =
Math.max(0, Math.min(mLeftColumn + deltaColumns, mColumns
- mVisibleColumns));
invalidate();
}
/**
* Sets the text size, which in turn sets the number of rows and columns.
*
* @param fontSize the new font size, in density-independent pixels.
*/
public void setTextSize(int fontSize) {
mTextSize = (int) (fontSize * mDensity);
updateText();
}
/**
* Sets style information about the cursor.
*
* @param style The style of the cursor.
* @param blink Whether the cursor should blink.
*/
public void setCursorStyle(int style, int blink) {
mCursorStyle = style;
if (blink != 0 && mCursorBlink == 0) {
mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD);
} else if (blink == 0 && mCursorBlink != 0) {
mHandler.removeCallbacks(mBlinkCursor);
}
mCursorBlink = blink;
}
/**
* Sets the IME mode ("cooked" or "raw").
*
* @param useCookedIME Whether the IME should be used in cooked mode.
*/
public void setUseCookedIME(boolean useCookedIME) {
mUseCookedIme = useCookedIME;
}
// Begin GestureDetector.OnGestureListener methods
public boolean onSingleTapUp(MotionEvent e) {
if (mExtGestureListener != null && mExtGestureListener.onSingleTapUp(e)) {
return true;
}
requestFocus();
return true;
}
public void onLongPress(MotionEvent e) {
// XXX hook into external gesture listener
showContextMenu();
}
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
if (mExtGestureListener != null && mExtGestureListener.onScroll(e1, e2, distanceX, distanceY)) {
return true;
}
distanceY += mScrollRemainder;
int deltaRows = (int) (distanceY / mCharacterHeight);
mScrollRemainder = distanceY - deltaRows * mCharacterHeight;
mTopRow =
Math.min(0, Math.max(-(mTranscriptScreen
.getActiveTranscriptRows()), mTopRow + deltaRows));
invalidate();
return true;
}
public void onSingleTapConfirmed(MotionEvent e) {
}
public boolean onJumpTapDown(MotionEvent e1, MotionEvent e2) {
// Scroll to bottom
mTopRow = 0;
invalidate();
return true;
}
public boolean onJumpTapUp(MotionEvent e1, MotionEvent e2) {
// Scroll to top
mTopRow = -mTranscriptScreen.getActiveTranscriptRows();
invalidate();
return true;
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (mExtGestureListener != null && mExtGestureListener.onFling(e1, e2, velocityX, velocityY)) {
return true;
}
float SCALE = 0.25f;
mScroller.fling(0, mTopRow,
-(int) (velocityX * SCALE), -(int) (velocityY * SCALE),
0, 0,
-mTranscriptScreen.getActiveTranscriptRows(), 0);
mScrollRemainder = 0.0f;
// onScroll(e1, e2, 0.1f * velocityX, -0.1f * velocityY);
post(mFlingRunner);
return true;
}
public void onShowPress(MotionEvent e) {
if (mExtGestureListener != null) {
mExtGestureListener.onShowPress(e);
}
}
public boolean onDown(MotionEvent e) {
if (mExtGestureListener != null && mExtGestureListener.onDown(e)) {
return true;
}
mScrollRemainder = 0.0f;
return true;
}
// End GestureDetector.OnGestureListener methods
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mIsSelectingText) {
return onTouchEventWhileSelectingText(ev);
} else {
return mGestureDetector.onTouchEvent(ev);
}
}
private boolean onTouchEventWhileSelectingText(MotionEvent ev) {
int action = ev.getAction();
int cx = (int)(ev.getX() / mCharacterWidth);
int cy = Math.max(0,
(int)((ev.getY() + SELECT_TEXT_OFFSET_Y * mScaledDensity)
/ mCharacterHeight) + mTopRow);
switch (action) {
case MotionEvent.ACTION_DOWN:
mSelXAnchor = cx;
mSelYAnchor = cy;
mSelX1 = cx;
mSelY1 = cy;
mSelX2 = mSelX1;
mSelY2 = mSelY1;
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
int minx = Math.min(mSelXAnchor, cx);
int maxx = Math.max(mSelXAnchor, cx);
int miny = Math.min(mSelYAnchor, cy);
int maxy = Math.max(mSelYAnchor, cy);
mSelX1 = minx;
mSelY1 = miny;
mSelX2 = maxx;
mSelY2 = maxy;
if (action == MotionEvent.ACTION_UP) {
ClipboardManagerCompat clip = ClipboardManagerCompatFactory
.getManager(getContext().getApplicationContext());
clip.setText(getSelectedText().trim());
toggleSelectingText();
}
invalidate();
break;
default:
toggleSelectingText();
invalidate();
break;
}
return true;
}
/**
* Called when a key is pressed in the view.
*
* @param keyCode The keycode of the key which was pressed.
* @param event A {@link KeyEvent} describing the event.
* @return Whether the event was handled.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) {
Log.w(TAG, "onKeyDown " + keyCode);
}
if (handleControlKey(keyCode, true)) {
return true;
} else if (handleFnKey(keyCode, true)) {
return true;
} else if (isSystemKey(keyCode, event)) {
if (! isInterceptedSystemKey(keyCode) ) {
// Don't intercept the system keys
return super.onKeyDown(keyCode, event);
}
}
// Translate the keyCode into an ASCII character.
try {
int oldCombiningAccent = mKeyListener.getCombiningAccent();
int oldCursorMode = mKeyListener.getCursorMode();
mKeyListener.keyDown(keyCode, event, getKeypadApplicationMode(),
TermKeyListener.isEventFromToggleDevice(event));
if (mKeyListener.getCombiningAccent() != oldCombiningAccent
|| mKeyListener.getCursorMode() != oldCursorMode) {
invalidate();
}
} catch (IOException e) {
// Ignore I/O exceptions
}
return true;
}
/** Do we want to intercept this system key? */
private boolean isInterceptedSystemKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_BACK && mBackKeySendsCharacter;
}
/**
* Called when a key is released in the view.
*
* @param keyCode The keycode of the key which was released.
* @param event A {@link KeyEvent} describing the event.
* @return Whether the event was handled.
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) {
Log.w(TAG, "onKeyUp " + keyCode);
}
if (handleControlKey(keyCode, false)) {
return true;
} else if (handleFnKey(keyCode, false)) {
return true;
} else if (isSystemKey(keyCode, event)) {
// Don't intercept the system keys
if ( ! isInterceptedSystemKey(keyCode) ) {
return super.onKeyUp(keyCode, event);
}
}
mKeyListener.keyUp(keyCode, event);
clearSpecialKeyStatus();
return true;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (sTrapAltAndMeta) {
boolean altEsc = mKeyListener.getAltSendsEsc();
boolean altOn = (event.getMetaState() & KeyEvent.META_ALT_ON) != 0;
boolean metaOn = (event.getMetaState() & KeyEvent.META_META_ON) != 0;
boolean altPressed = (keyCode == KeyEvent.KEYCODE_ALT_LEFT)
|| (keyCode == KeyEvent.KEYCODE_ALT_RIGHT);
boolean altActive = mKeyListener.isAltActive();
if (altEsc && (altOn || altPressed || altActive || metaOn)) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
return onKeyDown(keyCode, event);
} else {
return onKeyUp(keyCode, event);
}
}
}
if (handleHardwareControlKey(keyCode, event)) {
return true;
}
if (mKeyListener.isCtrlActive()) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
return onKeyDown(keyCode, event);
} else {
return onKeyUp(keyCode, event);
}
}
return super.onKeyPreIme(keyCode, event);
};
private boolean handleControlKey(int keyCode, boolean down) {
if (keyCode == mControlKeyCode) {
if (LOG_KEY_EVENTS) {
Log.w(TAG, "handleControlKey " + keyCode);
}
mKeyListener.handleControlKey(down);
invalidate();
return true;
}
return false;
}
private boolean handleHardwareControlKey(int keyCode, KeyEvent event) {
if (keyCode == TermKeyListener.KEYCODE_CTRL_LEFT ||
keyCode == TermKeyListener.KEYCODE_CTRL_RIGHT) {
if (LOG_KEY_EVENTS) {
Log.w(TAG, "handleHardwareControlKey " + keyCode);
}
boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
mKeyListener.handleHardwareControlKey(down);
invalidate();
return true;
}
return false;
}
private boolean handleFnKey(int keyCode, boolean down) {
if (keyCode == mFnKeyCode) {
if (LOG_KEY_EVENTS) {
Log.w(TAG, "handleFnKey " + keyCode);
}
mKeyListener.handleFnKey(down);
invalidate();
return true;
}
return false;
}
private boolean isSystemKey(int keyCode, KeyEvent event) {
return event.isSystem();
}
private void clearSpecialKeyStatus() {
if (mIsControlKeySent) {
mIsControlKeySent = false;
mKeyListener.handleControlKey(false);
invalidate();
}
if (mIsFnKeySent) {
mIsFnKeySent = false;
mKeyListener.handleFnKey(false);
invalidate();
}
}
private void updateText() {
ColorScheme scheme = mColorScheme;
if (mTextSize > 0) {
mTextRenderer = new PaintRenderer(mTextSize, scheme);
}
else {
mTextRenderer = new Bitmap4x8FontRenderer(getResources(), scheme);
}
mForegroundPaint.setColor(scheme.getForeColor());
mBackgroundPaint.setColor(scheme.getBackColor());
mCharacterWidth = mTextRenderer.getCharacterWidth();
mCharacterHeight = mTextRenderer.getCharacterHeight();
updateSize(true);
}
/**
* This is called during layout when the size of this view has changed. If
* you were just added to the view hierarchy, you're called with the old
* values of 0.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (mTermSession == null) {
// Not ready, defer until TermSession is attached
mDeferInit = true;
return;
}
if (!mKnownSize) {
mKnownSize = true;
initialize();
} else {
updateSize(false);
}
}
private void updateSize(int w, int h) {
mColumns = Math.max(1, (int) (((float) w) / mCharacterWidth));
mVisibleColumns = (int) (((float) mVisibleWidth) / mCharacterWidth);
mTopOfScreenMargin = mTextRenderer.getTopMargin();
mRows = Math.max(1, (h - mTopOfScreenMargin) / mCharacterHeight);
mTermSession.updateSize(mColumns, mRows);
// Reset our paging:
mTopRow = 0;
mLeftColumn = 0;
invalidate();
}
/**
* Update the view's idea of its size.
*
* @param force Whether a size adjustment should be performed even if the
* view's size has not changed.
*/
public void updateSize(boolean force) {
if (mKnownSize) {
int w = getWidth();
int h = getHeight();
// Log.w("Term", "(" + w + ", " + h + ")");
if (force || w != mVisibleWidth || h != mVisibleHeight) {
mVisibleWidth = w;
mVisibleHeight = h;
updateSize(mVisibleWidth, mVisibleHeight);
}
}
}
/**
* Draw the view to the provided {@link Canvas}.
*
* @param canvas The {@link Canvas} to draw the view to.
*/
@Override
protected void onDraw(Canvas canvas) {
updateSize(false);
if (mEmulator == null) {
// Not ready yet
return;
}
int w = getWidth();
int h = getHeight();
boolean reverseVideo = mEmulator.getReverseVideo();
mTextRenderer.setReverseVideo(reverseVideo);
Paint backgroundPaint =
reverseVideo ? mForegroundPaint : mBackgroundPaint;
canvas.drawRect(0, 0, w, h, backgroundPaint);
float x = -mLeftColumn * mCharacterWidth;
float y = mCharacterHeight + mTopOfScreenMargin;
int endLine = mTopRow + mRows;
int cx = mEmulator.getCursorCol();
int cy = mEmulator.getCursorRow();
boolean cursorVisible = mCursorVisible && mEmulator.getShowCursor();
String effectiveImeBuffer = mImeBuffer;
int combiningAccent = mKeyListener.getCombiningAccent();
if (combiningAccent != 0) {
effectiveImeBuffer += String.valueOf((char) combiningAccent);
}
int cursorStyle = mKeyListener.getCursorMode();
for (int i = mTopRow; i < endLine; i++) {
int cursorX = -1;
if (i == cy && cursorVisible) {
cursorX = cx;
}
int selx1 = -1;
int selx2 = -1;
if ( i >= mSelY1 && i <= mSelY2 ) {
if ( i == mSelY1 ) {
selx1 = mSelX1;
}
if ( i == mSelY2 ) {
selx2 = mSelX2;
} else {
selx2 = mColumns;
}
}
mTranscriptScreen.drawText(i, canvas, x, y, mTextRenderer, cursorX, selx1, selx2, effectiveImeBuffer, cursorStyle);
y += mCharacterHeight;
}
}
private void ensureCursorVisible() {
mTopRow = 0;
if (mVisibleColumns > 0) {
int cx = mEmulator.getCursorCol();
int visibleCursorX = mEmulator.getCursorCol() - mLeftColumn;
if (visibleCursorX < 0) {
mLeftColumn = cx;
} else if (visibleCursorX >= mVisibleColumns) {
mLeftColumn = (cx - mVisibleColumns) + 1;
}
}
}
/**
* Toggle text selection mode in the view.
*/
public void toggleSelectingText() {
mIsSelectingText = ! mIsSelectingText;
setVerticalScrollBarEnabled( ! mIsSelectingText );
if ( ! mIsSelectingText ) {
mSelX1 = -1;
mSelY1 = -1;
mSelX2 = -1;
mSelY2 = -1;
}
}
/**
* Whether the view is currently in text selection mode.
*/
public boolean getSelectingText() {
return mIsSelectingText;
}
/**
* Get selected text.
*
* @return A {@link String} with the selected text.
*/
public String getSelectedText() {
return mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2);
}
/**
* Send a Ctrl key event to the terminal.
*/
public void sendControlKey() {
mIsControlKeySent = true;
mKeyListener.handleControlKey(true);
invalidate();
}
/**
* Send an Fn key event to the terminal. The Fn modifier key can be used to
* generate various special characters and escape codes.
*/
public void sendFnKey() {
mIsFnKeySent = true;
mKeyListener.handleFnKey(true);
invalidate();
}
/**
* Set the key code to be sent when the Back key is pressed.
*/
public void setBackKeyCharacter(int keyCode) {
mKeyListener.setBackKeyCharacter(keyCode);
mBackKeySendsCharacter = (keyCode != 0);
}
/**
* Set whether to prepend the ESC keycode to the character when when pressing
* the ALT Key.
* @param flag
*/
public void setAltSendsEsc(boolean flag) {
mKeyListener.setAltSendsEsc(flag);
}
/**
* Set the keycode corresponding to the Ctrl key.
*/
public void setControlKeyCode(int keyCode) {
mControlKeyCode = keyCode;
}
/**
* Set the keycode corresponding to the Fn key.
*/
public void setFnKeyCode(int keyCode) {
mFnKeyCode = keyCode;
}
public void setTermType(String termType) {
mKeyListener.setTermType(termType);
mTermType = termType;
}
}