Home | History | Annotate | Download | only in terminal
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.terminal;
     18 
     19 import static com.android.terminal.Terminal.TAG;
     20 
     21 import android.content.Context;
     22 import android.graphics.Paint;
     23 import android.graphics.Paint.FontMetrics;
     24 import android.graphics.Typeface;
     25 import android.os.Parcelable;
     26 import android.util.AttributeSet;
     27 import android.util.Log;
     28 import android.view.KeyEvent;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.inputmethod.BaseInputConnection;
     32 import android.view.inputmethod.EditorInfo;
     33 import android.view.inputmethod.InputConnection;
     34 import android.view.inputmethod.InputMethodManager;
     35 import android.widget.AdapterView;
     36 import android.widget.BaseAdapter;
     37 import android.widget.ListView;
     38 
     39 import com.android.terminal.Terminal.CellRun;
     40 import com.android.terminal.Terminal.TerminalClient;
     41 
     42 /**
     43  * Rendered contents of a {@link Terminal} session.
     44  */
     45 public class TerminalView extends ListView {
     46     private static final boolean LOGD = true;
     47 
     48     private static final boolean SCROLL_ON_DAMAGE = false;
     49     private static final boolean SCROLL_ON_INPUT = true;
     50 
     51     private Terminal mTerm;
     52 
     53     private boolean mScrolled;
     54 
     55     private int mRows;
     56     private int mCols;
     57     private int mScrollRows;
     58 
     59     private final TerminalMetrics mMetrics = new TerminalMetrics();
     60     private final TerminalKeys mTermKeys = new TerminalKeys();
     61 
     62     /**
     63      * Metrics shared between all {@link TerminalLineView} children. Locking
     64      * provided by main thread.
     65      */
     66     static class TerminalMetrics {
     67         private static final int MAX_RUN_LENGTH = 128;
     68 
     69         final Paint bgPaint = new Paint();
     70         final Paint textPaint = new Paint();
     71         final Paint cursorPaint = new Paint();
     72 
     73         /** Run of cells used when drawing */
     74         final CellRun run;
     75         /** Screen coordinates to draw chars into */
     76         final float[] pos;
     77 
     78         int charTop;
     79         int charWidth;
     80         int charHeight;
     81 
     82         public TerminalMetrics() {
     83             run = new Terminal.CellRun();
     84             run.data = new char[MAX_RUN_LENGTH];
     85 
     86             // Positions of each possible cell
     87             // TODO: make sure this works with surrogate pairs
     88             pos = new float[MAX_RUN_LENGTH * 2];
     89             setTextSize(20);
     90         }
     91 
     92         public void setTextSize(float textSize) {
     93             textPaint.setTypeface(Typeface.MONOSPACE);
     94             textPaint.setAntiAlias(true);
     95             textPaint.setTextSize(textSize);
     96 
     97             // Read metrics to get exact pixel dimensions
     98             final FontMetrics fm = textPaint.getFontMetrics();
     99             charTop = (int) Math.ceil(fm.top);
    100 
    101             final float[] widths = new float[1];
    102             textPaint.getTextWidths("X", widths);
    103             charWidth = (int) Math.ceil(widths[0]);
    104             charHeight = (int) Math.ceil(fm.descent - fm.top);
    105 
    106             // Update drawing positions
    107             for (int i = 0; i < MAX_RUN_LENGTH; i++) {
    108                 pos[i * 2] = i * charWidth;
    109                 pos[(i * 2) + 1] = -charTop;
    110             }
    111         }
    112     }
    113 
    114     private final AdapterView.OnItemClickListener mClickListener = new AdapterView.OnItemClickListener() {
    115         @Override
    116         public void onItemClick(AdapterView<?> parent, View v, int pos, long id) {
    117             if (parent.requestFocus()) {
    118                 InputMethodManager imm = (InputMethodManager) parent.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    119                 imm.showSoftInput(parent, InputMethodManager.SHOW_IMPLICIT);
    120             }
    121         }
    122     };
    123 
    124     private final Runnable mDamageRunnable = new Runnable() {
    125         @Override
    126         public void run() {
    127             invalidateViews();
    128             if (SCROLL_ON_DAMAGE) {
    129                 scrollToBottom(true);
    130             }
    131         }
    132     };
    133 
    134     public TerminalView(Context context) {
    135         this(context, null);
    136     }
    137 
    138     public TerminalView(Context context, AttributeSet attrs) {
    139         this(context, attrs, com.android.internal.R.attr.listViewStyle);
    140     }
    141 
    142     public TerminalView(Context context, AttributeSet attrs, int defStyle) {
    143         super(context, attrs, defStyle);
    144 
    145         setBackground(null);
    146         setDivider(null);
    147 
    148         setFocusable(true);
    149         setFocusableInTouchMode(true);
    150 
    151         setAdapter(mAdapter);
    152         setOnKeyListener(mKeyListener);
    153 
    154         setOnItemClickListener(mClickListener);
    155     }
    156 
    157     private final BaseAdapter mAdapter = new BaseAdapter() {
    158         @Override
    159         public View getView(int position, View convertView, ViewGroup parent) {
    160             final TerminalLineView view;
    161             if (convertView != null) {
    162                 view = (TerminalLineView) convertView;
    163             } else {
    164                 view = new TerminalLineView(parent.getContext(), mTerm, mMetrics);
    165             }
    166 
    167             view.pos = position;
    168             view.row = posToRow(position);
    169             view.cols = mCols;
    170             return view;
    171         }
    172 
    173         @Override
    174         public long getItemId(int position) {
    175             return position;
    176         }
    177 
    178         @Override
    179         public Object getItem(int position) {
    180             return null;
    181         }
    182 
    183         @Override
    184         public int getCount() {
    185             if (mTerm != null) {
    186                 return mRows + mScrollRows;
    187             } else {
    188                 return 0;
    189             }
    190         }
    191     };
    192 
    193     private TerminalClient mClient = new TerminalClient() {
    194         @Override
    195         public void onDamage(final int startRow, final int endRow, int startCol, int endCol) {
    196             post(mDamageRunnable);
    197         }
    198 
    199         @Override
    200         public void onMoveRect(int destStartRow, int destEndRow, int destStartCol, int destEndCol,
    201                 int srcStartRow, int srcEndRow, int srcStartCol, int srcEndCol) {
    202             post(mDamageRunnable);
    203         }
    204 
    205         @Override
    206         public void onMoveCursor(int posRow, int posCol, int oldPosRow, int oldPosCol, int visible) {
    207             post(mDamageRunnable);
    208         }
    209 
    210         @Override
    211         public void onBell() {
    212             Log.i(TAG, "DING!");
    213         }
    214     };
    215 
    216     private int rowToPos(int row) {
    217         return row + mScrollRows;
    218     }
    219 
    220     private int posToRow(int pos) {
    221         return pos - mScrollRows;
    222     }
    223 
    224     private View.OnKeyListener mKeyListener = new OnKeyListener() {
    225         @Override
    226         public boolean onKey(View v, int keyCode, KeyEvent event) {
    227             final boolean res = mTermKeys.onKey(v, keyCode, event);
    228             if (res && SCROLL_ON_INPUT) {
    229                 scrollToBottom(true);
    230             }
    231             return res;
    232         }
    233     };
    234 
    235     @Override
    236     public void onRestoreInstanceState(Parcelable state) {
    237         super.onRestoreInstanceState(state);
    238         mScrolled = true;
    239     }
    240 
    241     @Override
    242     protected void onAttachedToWindow() {
    243         super.onAttachedToWindow();
    244         if (!mScrolled) {
    245             scrollToBottom(false);
    246         }
    247     }
    248 
    249     @Override
    250     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    251         super.onSizeChanged(w, h, oldw, oldh);
    252 
    253         final int rows = h / mMetrics.charHeight;
    254         final int cols = w / mMetrics.charWidth;
    255         final int scrollRows = mScrollRows;
    256 
    257         final boolean sizeChanged = (rows != mRows || cols != mCols || scrollRows != mScrollRows);
    258         if (mTerm != null && sizeChanged) {
    259             mTerm.resize(rows, cols, scrollRows);
    260 
    261             mRows = rows;
    262             mCols = cols;
    263             mScrollRows = scrollRows;
    264 
    265             mAdapter.notifyDataSetChanged();
    266         }
    267     }
    268 
    269     public void scrollToBottom(boolean animate) {
    270         final int dur = animate ? 250 : 0;
    271         smoothScrollToPositionFromTop(getCount(), 0, dur);
    272         mScrolled = true;
    273     }
    274 
    275     public void setTerminal(Terminal term) {
    276         final Terminal orig = mTerm;
    277         if (orig != null) {
    278             orig.setClient(null);
    279         }
    280         mTerm = term;
    281         mScrolled = false;
    282         if (term != null) {
    283             term.setClient(mClient);
    284             mTermKeys.setTerminal(term);
    285 
    286             mMetrics.cursorPaint.setColor(0xfff0f0f0);
    287 
    288             // Populate any current settings
    289             mRows = mTerm.getRows();
    290             mCols = mTerm.getCols();
    291             mScrollRows = mTerm.getScrollRows();
    292             mAdapter.notifyDataSetChanged();
    293         }
    294     }
    295 
    296     public Terminal getTerminal() {
    297         return mTerm;
    298     }
    299 
    300     public void setTextSize(float textSize) {
    301         mMetrics.setTextSize(textSize);
    302 
    303         // Layout will kick off terminal resize when needed
    304         requestLayout();
    305     }
    306 
    307     @Override
    308     public boolean onCheckIsTextEditor() {
    309         return true;
    310     }
    311 
    312     @Override
    313     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    314         outAttrs.imeOptions |=
    315             EditorInfo.IME_FLAG_NO_EXTRACT_UI |
    316             EditorInfo.IME_FLAG_NO_ENTER_ACTION |
    317             EditorInfo.IME_ACTION_NONE;
    318         outAttrs.inputType = EditorInfo.TYPE_NULL;
    319         return new BaseInputConnection(this, false) {
    320             @Override
    321             public boolean deleteSurroundingText (int leftLength, int rightLength) {
    322                 KeyEvent k;
    323                 if (rightLength == 0 && leftLength == 0) {
    324                     k = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
    325                     return this.sendKeyEvent(k);
    326                 }
    327                 for (int i = 0; i < leftLength; i++) {
    328                     k = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
    329                     this.sendKeyEvent(k);
    330                 }
    331                 for (int i = 0; i < rightLength; i++) {
    332                     k = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL);
    333                     this.sendKeyEvent(k);
    334                 }
    335                 return true;
    336             }
    337         };
    338     }
    339 }
    340