blob: 076da80d6d5059c53346168caf406b0a54253c0a [file] [log] [blame]
/*
* Copyright (C) 2013 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 com.android.terminal;
import static com.android.terminal.Terminal.TAG;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Typeface;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import com.android.terminal.Terminal.CellRun;
import com.android.terminal.Terminal.TerminalClient;
/**
* Rendered contents of a {@link Terminal} session.
*/
public class TerminalView extends ListView {
private static final boolean LOGD = true;
private static final boolean SCROLL_ON_DAMAGE = false;
private static final boolean SCROLL_ON_INPUT = true;
private Terminal mTerm;
private boolean mScrolled;
private int mRows;
private int mCols;
private int mScrollRows;
private final TerminalMetrics mMetrics = new TerminalMetrics();
private final TerminalKeys mTermKeys = new TerminalKeys();
/**
* Metrics shared between all {@link TerminalLineView} children. Locking
* provided by main thread.
*/
static class TerminalMetrics {
private static final int MAX_RUN_LENGTH = 128;
final Paint bgPaint = new Paint();
final Paint textPaint = new Paint();
final Paint cursorPaint = new Paint();
/** Run of cells used when drawing */
final CellRun run;
/** Screen coordinates to draw chars into */
final float[] pos;
int charTop;
int charWidth;
int charHeight;
public TerminalMetrics() {
run = new Terminal.CellRun();
run.data = new char[MAX_RUN_LENGTH];
// Positions of each possible cell
// TODO: make sure this works with surrogate pairs
pos = new float[MAX_RUN_LENGTH * 2];
setTextSize(20);
}
public void setTextSize(float textSize) {
textPaint.setTypeface(Typeface.MONOSPACE);
textPaint.setAntiAlias(true);
textPaint.setTextSize(textSize);
// Read metrics to get exact pixel dimensions
final FontMetrics fm = textPaint.getFontMetrics();
charTop = (int) Math.ceil(fm.top);
final float[] widths = new float[1];
textPaint.getTextWidths("X", widths);
charWidth = (int) Math.ceil(widths[0]);
charHeight = (int) Math.ceil(fm.descent - fm.top);
// Update drawing positions
for (int i = 0; i < MAX_RUN_LENGTH; i++) {
pos[i * 2] = i * charWidth;
pos[(i * 2) + 1] = -charTop;
}
}
}
private final AdapterView.OnItemClickListener mClickListener = new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int pos, long id) {
if (parent.requestFocus()) {
InputMethodManager imm = (InputMethodManager) parent.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(parent, InputMethodManager.SHOW_IMPLICIT);
}
}
};
private final Runnable mDamageRunnable = new Runnable() {
@Override
public void run() {
invalidateViews();
if (SCROLL_ON_DAMAGE) {
scrollToBottom(true);
}
}
};
public TerminalView(Context context) {
this(context, null);
}
public TerminalView(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.listViewStyle);
}
public TerminalView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setBackground(null);
setDivider(null);
setFocusable(true);
setFocusableInTouchMode(true);
setAdapter(mAdapter);
setOnKeyListener(mKeyListener);
setOnItemClickListener(mClickListener);
}
private final BaseAdapter mAdapter = new BaseAdapter() {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final TerminalLineView view;
if (convertView != null) {
view = (TerminalLineView) convertView;
} else {
view = new TerminalLineView(parent.getContext(), mTerm, mMetrics);
}
view.pos = position;
view.row = posToRow(position);
view.cols = mCols;
return view;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public int getCount() {
if (mTerm != null) {
return mRows + mScrollRows;
} else {
return 0;
}
}
};
private TerminalClient mClient = new TerminalClient() {
@Override
public void onDamage(final int startRow, final int endRow, int startCol, int endCol) {
post(mDamageRunnable);
}
@Override
public void onMoveRect(int destStartRow, int destEndRow, int destStartCol, int destEndCol,
int srcStartRow, int srcEndRow, int srcStartCol, int srcEndCol) {
post(mDamageRunnable);
}
@Override
public void onMoveCursor(int posRow, int posCol, int oldPosRow, int oldPosCol, int visible) {
post(mDamageRunnable);
}
@Override
public void onBell() {
Log.i(TAG, "DING!");
}
};
private int rowToPos(int row) {
return row + mScrollRows;
}
private int posToRow(int pos) {
return pos - mScrollRows;
}
private View.OnKeyListener mKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
final boolean res = mTermKeys.onKey(v, keyCode, event);
if (res && SCROLL_ON_INPUT) {
scrollToBottom(true);
}
return res;
}
};
@Override
public void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
mScrolled = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mScrolled) {
scrollToBottom(false);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
final int rows = h / mMetrics.charHeight;
final int cols = w / mMetrics.charWidth;
final int scrollRows = mScrollRows;
final boolean sizeChanged = (rows != mRows || cols != mCols || scrollRows != mScrollRows);
if (mTerm != null && sizeChanged) {
mTerm.resize(rows, cols, scrollRows);
mRows = rows;
mCols = cols;
mScrollRows = scrollRows;
mAdapter.notifyDataSetChanged();
}
}
public void scrollToBottom(boolean animate) {
final int dur = animate ? 250 : 0;
smoothScrollToPositionFromTop(getCount(), 0, dur);
mScrolled = true;
}
public void setTerminal(Terminal term) {
final Terminal orig = mTerm;
if (orig != null) {
orig.setClient(null);
}
mTerm = term;
mScrolled = false;
if (term != null) {
term.setClient(mClient);
mTermKeys.setTerminal(term);
mMetrics.cursorPaint.setColor(0xfff0f0f0);
// Populate any current settings
mRows = mTerm.getRows();
mCols = mTerm.getCols();
mScrollRows = mTerm.getScrollRows();
mAdapter.notifyDataSetChanged();
}
}
public Terminal getTerminal() {
return mTerm;
}
public void setTextSize(float textSize) {
mMetrics.setTextSize(textSize);
// Layout will kick off terminal resize when needed
requestLayout();
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.imeOptions |=
EditorInfo.IME_FLAG_NO_EXTRACT_UI |
EditorInfo.IME_FLAG_NO_ENTER_ACTION |
EditorInfo.IME_ACTION_NONE;
outAttrs.inputType = EditorInfo.TYPE_NULL;
return new BaseInputConnection(this, false) {
@Override
public boolean deleteSurroundingText (int leftLength, int rightLength) {
KeyEvent k;
if (rightLength == 0 && leftLength == 0) {
k = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
return this.sendKeyEvent(k);
}
for (int i = 0; i < leftLength; i++) {
k = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
this.sendKeyEvent(k);
}
for (int i = 0; i < rightLength; i++) {
k = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL);
this.sendKeyEvent(k);
}
return true;
}
};
}
}