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