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
17package com.android.terminal;
18
19import static com.android.terminal.Terminal.TAG;
20
21import android.content.Context;
22import android.graphics.Paint;
23import android.graphics.Paint.FontMetrics;
24import android.graphics.Typeface;
25import android.os.Parcelable;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.KeyEvent;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.inputmethod.BaseInputConnection;
32import android.view.inputmethod.EditorInfo;
33import android.view.inputmethod.InputConnection;
34import android.view.inputmethod.InputMethodManager;
35import android.widget.AdapterView;
36import android.widget.BaseAdapter;
37import android.widget.ListView;
38
39import com.android.terminal.Terminal.CellRun;
40import com.android.terminal.Terminal.TerminalClient;
41
42/**
43 * Rendered contents of a {@link Terminal} session.
44 */
45public 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