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 
TerminalMetrics()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 
setTextSize(float textSize)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 
TerminalView(Context context)134     public TerminalView(Context context) {
135         this(context, null);
136     }
137 
TerminalView(Context context, AttributeSet attrs)138     public TerminalView(Context context, AttributeSet attrs) {
139         this(context, attrs, com.android.internal.R.attr.listViewStyle);
140     }
141 
TerminalView(Context context, AttributeSet attrs, int defStyle)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 
rowToPos(int row)216     private int rowToPos(int row) {
217         return row + mScrollRows;
218     }
219 
posToRow(int pos)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
onRestoreInstanceState(Parcelable state)236     public void onRestoreInstanceState(Parcelable state) {
237         super.onRestoreInstanceState(state);
238         mScrolled = true;
239     }
240 
241     @Override
onAttachedToWindow()242     protected void onAttachedToWindow() {
243         super.onAttachedToWindow();
244         if (!mScrolled) {
245             scrollToBottom(false);
246         }
247     }
248 
249     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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 
scrollToBottom(boolean animate)269     public void scrollToBottom(boolean animate) {
270         final int dur = animate ? 250 : 0;
271         smoothScrollToPositionFromTop(getCount(), 0, dur);
272         mScrolled = true;
273     }
274 
setTerminal(Terminal term)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 
getTerminal()296     public Terminal getTerminal() {
297         return mTerm;
298     }
299 
setTextSize(float textSize)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
onCheckIsTextEditor()308     public boolean onCheckIsTextEditor() {
309         return true;
310     }
311 
312     @Override
onCreateInputConnection(EditorInfo outAttrs)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