/* * 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 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.ClipboardManager; 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; /** * Used to paint the cursor */ private Paint mCursorPaint; 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; private boolean mIsActive = false; /** * 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; mCursorPaint = new Paint(); mCursorPaint.setARGB(255,128,128,128); 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() { mIsActive = true; updateSize(false); if (mCursorBlink != 0) { mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD); } } /** * Inform the view that it is no longer visible on the screen. */ public void onPause() { if (mCursorBlink != 0) { mHandler.removeCallbacks(mBlinkCursor); } mIsActive = false; } /** * 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 arg0) { if (LOG_IME) { Log.w(TAG, "getCursorCapsMode(" + arg0 + ")"); } return 0; } 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); if(mTextSize < 10) mTextSize = 10; 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); } } @SuppressWarnings("deprecation") 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) { ClipboardManager clip = (ClipboardManager) getContext().getApplicationContext() .getSystemService(Context.CLIPBOARD_SERVICE); 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(); mKeyListener.keyDown(keyCode, event, getKeypadApplicationMode(), TermKeyListener.isEventFromToggleDevice(event)); if (mKeyListener.getCombiningAccent() != oldCombiningAccent) { 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.getAction() == KeyEvent.ACTION_DOWN)) { 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); return true; } return false; } private boolean handleHardwareControlKey(int keyCode, boolean down) { if (keyCode == TermKeyListener.KEYCODE_CTRL_LEFT || keyCode == TermKeyListener.KEYCODE_CTRL_RIGHT) { if (LOG_KEY_EVENTS) { Log.w(TAG, "handleControlKey " + keyCode); } mKeyListener.handleControlKey(down); 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); return true; } return false; } private boolean isSystemKey(int keyCode, KeyEvent event) { return event.isSystem(); } private void clearSpecialKeyStatus() { if (mIsControlKeySent) { mIsControlKeySent = false; mKeyListener.handleControlKey(false); } if (mIsFnKeySent) { mIsFnKeySent = false; mKeyListener.handleFnKey(false); } } 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); } 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); 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); } /** * 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); } /** * 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; } }