package org.gscript.terminal;
import java.nio.ByteBuffer;
import org.gscript.settings.ShellProfile;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
public class EmulatorScreen extends View {
public static final String LOG_TAG = "EmulatorView";
public static final int GESTURE_MODE_SCROLL = 0;
public static final int GESTURE_MODE_CURSOR = 1;
static final boolean CURSOR_REGION_DEBUG = false;
static final int GESTURE_BORDER_DIP = 75;
static final int CURSOR_REPEAT_DELAY = 300;
static final int CURSOR_DIRECTION_LEFT = 0;
static final int CURSOR_DIRECTION_RIGHT = 1;
static final int CURSOR_DIRECTION_UP = 2;
static final int CURSOR_DIRECTION_DOWN = 3;
ScreenBufferParcelable mScreenBuffer;
boolean mAutoScrollEnabled = true;
boolean mQuickScrollEnabled = true;
boolean mQuickScroll = false;
boolean mResizing = false;
volatile int mUpdateRequested;
int mCanvasPadding;
ByteBuffer mRowBuffer;
char[] mCharSequence;
float mCharDecent;
float mCharWidth;
float mLineHeight;
int mTextColor;
int mBackColor;
int mScreenHeight;
int mScreenWidth;
int mScreenFillTop;
int mScreenRows;
int mScreenCols;
Paint mBackgroundPaint = new Paint();
Paint mForegroundPaint = new Paint();
int mGestureMode = GESTURE_MODE_SCROLL;
int mGestureBorderSize;
boolean mCursorVisible;
RectF[] mCursorRegions = new RectF[4];
int mCursorDirection;
int mCursorPointers;
GestureDetector mGestureDetector;
int mPrevScrollY;
Scroller mScroller;
EmulatorScreenListener mListener;
public EmulatorScreen(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initializeView();
}
public EmulatorScreen(Context context, AttributeSet attrs) {
super(context, attrs);
initializeView();
}
public EmulatorScreen(Context context) {
super(context);
initializeView();
}
void initializeView() {
setScrollContainer(true);
setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
setScrollbarFadingEnabled(true);
mGestureDetector = new GestureDetector(getContext(),
new GestureListener());
mScroller = new Scroller(getContext());
/* initialize empty regions so that they can be set in onLayout */
for(int i=0; i < mCursorRegions.length; ++i)
mCursorRegions[i] = new RectF();
}
public void initialize(ShellProfile profile,
ScreenBufferParcelable screenBuffer) {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mScreenBuffer = screenBuffer;
mTextColor = mScreenBuffer.getTextColor();
mBackColor = mScreenBuffer.getBackColor();
mBackgroundPaint.setColor(ColorScheme.getColor(mBackColor, false));
mBackgroundPaint.setStyle(Style.FILL);
/* also update the window background color if possible */
final Context context = getContext();
if (context instanceof Activity) {
Activity parent = (Activity) context;
parent.getWindow().setBackgroundDrawable(
new ColorDrawable(ColorScheme.getColor(mBackColor, false)));
}
int textSize = Integer.parseInt(profile.fontsize);
float textSizeSp = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, textSize, metrics);
mForegroundPaint.setColor(ColorScheme.getColor(mTextColor, false));
mForegroundPaint.setStyle(Style.FILL);
mForegroundPaint.setTextAlign(Align.LEFT);
mForegroundPaint.setTypeface(Typeface.MONOSPACE);
mForegroundPaint.setUnderlineText(false);
mForegroundPaint.setStrikeThruText(false);
mForegroundPaint.setFakeBoldText(false);
mForegroundPaint.setAntiAlias(true);
mForegroundPaint.setTextSize(textSizeSp);
mCanvasPadding = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 10, metrics);
mLineHeight = mForegroundPaint.getFontSpacing();
mCharWidth = mForegroundPaint.measureText("A");
mCharDecent = mForegroundPaint.descent();
mUpdateRequested++;
post(UpdateRunnable);
}
public boolean getCursorVisible() {
return mCursorVisible;
}
public void setCursorVisible(boolean visible) {
if(mCursorVisible != visible) {
mCursorVisible = visible;
invalidate();
}
}
public int getGestureMode() {
return mGestureMode;
}
public void setGestureMode(int mode) {
mGestureMode = mode;
if (mListener != null)
mListener.onGestureModeChanged(mGestureMode);
}
public boolean getAutoScrollEnabled() {
return mAutoScrollEnabled;
}
public void setAutoScrollEnabled(boolean enabled) {
mAutoScrollEnabled = enabled;
/* update scroll position when auto-scroll is enabled */
if (mAutoScrollEnabled) {
if (!mScroller.isFinished())
mScroller.forceFinished(true);
scrollTo(0, (int) (mScreenFillTop * mLineHeight));
}
if (mListener != null)
mListener.onAutoScrollChanged(mAutoScrollEnabled);
}
public boolean toggleAutoScrollEnabled() {
setAutoScrollEnabled(!mAutoScrollEnabled);
return mAutoScrollEnabled;
}
public boolean getQuickScrollEnabled() {
return mQuickScrollEnabled;
}
public void setQuickScrollEnabled(boolean enabled) {
mQuickScrollEnabled = enabled;
}
public boolean toggleQuickScrollEnabled() {
setQuickScrollEnabled(!mQuickScrollEnabled);
return mQuickScrollEnabled;
}
void toggleSoftInput() {
InputMethodManager imm = (InputMethodManager) getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
}
@Override
protected int computeVerticalScrollRange() {
return (int) ((mScreenFillTop + mScreenRows) * mLineHeight);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
if (mPrevScrollY == -1) {
mPrevScrollY = mScroller.getStartY();
}
int dy = mScroller.getCurrY() - mPrevScrollY;
mPrevScrollY = mScroller.getCurrX();
mPrevScrollY = mScroller.getCurrY();
scrollBy(dy);
} else {
mPrevScrollY = -1;
}
}
private void scrollBy(float dy) {
float scrollY = this.getScrollY();
scrollY += dy;
if (scrollY < 0)
scrollY = 0;
float scrollRange = computeVerticalScrollRange()
- (mScreenRows * mLineHeight);
if (scrollY > scrollRange)
scrollY = scrollRange;
this.scrollTo(0, (int) scrollY);
}
private class GestureListener extends SimpleOnGestureListener {
@Override
public boolean onDoubleTap(MotionEvent e) {
if(e.getY() < mGestureBorderSize) {
/* scroll to top */
setAutoScrollEnabled(false);
scrollTo(0, 0);
} else if(e.getY() > (getHeight() - mGestureBorderSize)) {
/* scroll to bottom */
setAutoScrollEnabled(true);
} else {
toggleSoftInput();
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
/* stop scrolling */
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
return true;
}
return super.onDown(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
switch (mGestureMode) {
case GESTURE_MODE_SCROLL:
setAutoScrollEnabled(false);
/*
* check if we need to handle a quick scroll based on the first
* event
*/
mQuickScroll = mQuickScrollEnabled
&& e1.getX() > (getWidth() - 100);
if (mQuickScroll) {
float evY = Math.min(Math.max(e2.getY(), 0), getHeight());
int row = (int) (((float) mScreenFillTop / 100f) * (100f / (float) (getHeight() - 1) * evY));
row = Math.min(Math.max(row, 0), mScreenFillTop);
scrollTo(0, (int) (row * mLineHeight));
} else {
/* normal scroll */
scrollBy(distanceY);
}
break;
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
switch (mGestureMode) {
case GESTURE_MODE_SCROLL:
/* do not fling on a quickscroll event */
if(!mQuickScroll) {
setAutoScrollEnabled(false);
int max_top = (int) (computeVerticalScrollRange() - (mScreenRows * mLineHeight));
int min_top = 0;
int startY = getScrollY();
mScroller.fling(0, startY, (int) -velocityX, (int) -velocityY,
0, 0, min_top, max_top);
invalidate();
}
break;
case GESTURE_MODE_CURSOR:
if(Math.abs(velocityX) > Math.abs(velocityY)) {
/* home/end */
if(velocityX > 0) {
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_MOVE_END));
} else {
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_MOVE_HOME));
}
} else {
/* page up/down */
if(velocityY > 0) {
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_PAGE_UP));
} else {
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_PAGE_DOWN));
}
}
break;
}
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (mGestureMode) {
case GESTURE_MODE_SCROLL:
mGestureDetector.onTouchEvent(event);
break;
case GESTURE_MODE_CURSOR:
if (event.getAction() == MotionEvent.ACTION_DOWN
|| event.getPointerCount() != mCursorPointers) {
removeCallbacks(CursorModeRunnable);
mCursorPointers = event.getPointerCount();
if(getHeight() < getWidth()) {
/* left / right first */
for(int i=0; i < mCursorRegions.length; ++i) {
if(mCursorRegions[i].contains(event.getX(), event.getY())) {
mCursorDirection = i;
post(CursorModeRunnable);
return true;
}
}
} else {
/* handle up/down first */
for(int i=mCursorRegions.length-1; i >= 0; --i) {
if(mCursorRegions[i].contains(event.getX(), event.getY())) {
mCursorDirection = i;
post(CursorModeRunnable);
return true;
}
}
}
mGestureDetector.onTouchEvent(event);
} else if (event.getAction() == MotionEvent.ACTION_UP) {
removeCallbacks(CursorModeRunnable);
}
mGestureDetector.onTouchEvent(event);
break;
}
return true;
}
private Runnable CursorModeRunnable = new Runnable() {
@Override
public void run() {
switch (mCursorDirection) {
case CURSOR_DIRECTION_LEFT:
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_DPAD_LEFT));
break;
case CURSOR_DIRECTION_RIGHT:
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_DPAD_RIGHT));
break;
case CURSOR_DIRECTION_UP:
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_DPAD_UP));
break;
case CURSOR_DIRECTION_DOWN:
sendBytes(EmulatorInput.KeyBinding.get(EmulatorInput.KEYCODE_DPAD_DOWN));
break;
}
postDelayed(this, CURSOR_REPEAT_DELAY / mCursorPointers);
}
};
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
if (changed) {
/* reset screen rows and cols so that a resize will get triggered */
mScreenRows = 0;
mScreenCols = 0;
mScreenWidth = right - left;
mScreenHeight = bottom - top;
int maxSize = (mScreenWidth > mScreenHeight) ? mScreenHeight / 4 : mScreenWidth / 4;
mGestureBorderSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, GESTURE_BORDER_DIP, getResources().getDisplayMetrics());
mGestureBorderSize = Math.min(mGestureBorderSize, maxSize);
/* set cursor regions */
mCursorRegions[CURSOR_DIRECTION_LEFT].set(0, 0, mGestureBorderSize, mScreenHeight);
mCursorRegions[CURSOR_DIRECTION_RIGHT].set(mScreenWidth-mGestureBorderSize, 0, mScreenWidth, mScreenHeight);
mCursorRegions[CURSOR_DIRECTION_UP].set(0, 0, mScreenWidth, mGestureBorderSize);
mCursorRegions[CURSOR_DIRECTION_DOWN].set(0, mScreenHeight-mGestureBorderSize, mScreenWidth, mScreenHeight);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/* clear screen */
canvas.drawPaint(mBackgroundPaint);
if (mScreenBuffer != null && !mResizing) {
if (mScreenRows == 0 || mScreenCols == 0) {
int height = (mScreenHeight - mCanvasPadding);
int width = (mScreenWidth - mCanvasPadding);
mScreenRows = (int) Math.floor((float) height / mLineHeight);
mScreenCols = (int) Math.floor((float) width / mCharWidth);
/* calculate real line height and char offsets */
width = width - (int) ((float) width / (float) mScreenCols);
height = height - (int) ((float) height / (float) mScreenRows);
if (mScreenBuffer.getScreenRows() != mScreenRows
|| mScreenBuffer.getScreenCols() != mScreenCols) {
mResizing = true;
/* request screenbuffer resize */
removeCallbacks(UpdateRunnable);
if (mListener != null) {
mListener.onWindowSizeChangeRequested(mScreenRows,
mScreenCols, canvas.getWidth(),
canvas.getHeight());
return;
}
}
}
if (mCharSequence == null || mCharSequence.length < mScreenCols)
mCharSequence = new char[mScreenCols];
int scrollTopRow = (int) Math
.floor(((float) getScrollY() + (mLineHeight - 1))
/ mLineHeight);
int cursorRow = mScreenBuffer.getCursorRow(true);
int cursorCol = mScreenBuffer.getCursorCol();
for (int row = scrollTopRow; row < (scrollTopRow + mScreenRows); ++row) {
mRowBuffer = mScreenBuffer.getRowData(mRowBuffer, row, true);
if (mRowBuffer == null)
return;
int rowFlags = mRowBuffer.getInt();
if (rowFlags != -1) {
int pos = mRowBuffer.position();
int cols = (mRowBuffer.capacity() - pos) / 4;
float rowOffset = (mCanvasPadding / 2)
+ (row * mLineHeight) - mCharDecent;
int sequenceEncoding = 0;
int sequenceLength = 0;
float sequenceOffset = 0;
for (int col = 0; col < cols; ++col) {
float offsetX = (mCanvasPadding / 2)
+ (col * mCharWidth);
if (row == cursorRow && col == cursorCol && mCursorVisible) {
mBackgroundPaint.setColor(ColorScheme.getColor(
mTextColor, true));
canvas.drawRect(
(offsetX + 1),
(rowOffset + 1) + mCharDecent,
(offsetX - 1) + mCharWidth,
(rowOffset - 1) + mLineHeight + mCharDecent,
mBackgroundPaint);
}
int encodedChar = mRowBuffer.getInt();
/*
* try to break line in to sequences with matching
* colors and text options
*/
if (sequenceEncoding == (encodedChar & 0x00ffffff)) {
char bChar = (char) ((encodedChar >> 24) & 0xff);
mCharSequence[sequenceLength] = bChar;
sequenceLength++;
} else {
if (sequenceLength > 0) {
/* render previous sequence */
byte bTextEffects = (byte) ((sequenceEncoding >> 16) & 0xff);
if ((bTextEffects & TextEffects.IGNORE) != TextEffects.IGNORE) {
byte bBackColor = (byte) (sequenceEncoding & 0xff);
byte bTextColor = (byte) ((sequenceEncoding >> 8) & 0xff);
mForegroundPaint
.setFakeBoldText(TextEffects
.hasTextEffect(
bTextEffects,
TextEffects.BOLD));
mForegroundPaint
.setUnderlineText(TextEffects
.hasTextEffect(
bTextEffects,
TextEffects.UNDERLINE));
mForegroundPaint
.setTextSkewX(TextEffects
.hasTextEffect(
bTextEffects,
TextEffects.ITALIC) ? -0.25f
: 0);
mBackgroundPaint.setColor(ColorScheme
.getColor(bBackColor, false));
mForegroundPaint
.setColor(ColorScheme.getColor(
bTextColor,
(bTextColor == bBackColor)));
canvas.drawText(mCharSequence, 0,
sequenceLength, sequenceOffset,
rowOffset + mLineHeight,
mForegroundPaint);
}
}
sequenceLength = 0;
sequenceEncoding = (encodedChar & 0x00ffffff);
sequenceOffset = offsetX;
byte bChar = (byte) ((encodedChar >> 24) & 0xff);
mCharSequence[sequenceLength] = (char) bChar;
sequenceLength++;
}
}
/* render possible remaining sequence */
if (sequenceLength > 0) {
byte bTextEffects = (byte) ((sequenceEncoding >> 16) & 0xff);
if ((bTextEffects & TextEffects.IGNORE) != TextEffects.IGNORE) {
byte bBackColor = (byte) (sequenceEncoding & 0xff);
byte bTextColor = (byte) ((sequenceEncoding >> 8) & 0xff);
mForegroundPaint.setFakeBoldText(TextEffects
.hasTextEffect(bTextEffects,
TextEffects.BOLD));
mForegroundPaint.setUnderlineText(TextEffects
.hasTextEffect(bTextEffects,
TextEffects.UNDERLINE));
mForegroundPaint.setTextSkewX(TextEffects
.hasTextEffect(bTextEffects,
TextEffects.ITALIC) ? -0.25f : 0);
mBackgroundPaint.setColor(ColorScheme.getColor(
bBackColor, false));
mForegroundPaint.setColor(ColorScheme.getColor(
bTextColor, (bTextColor == bBackColor)));
canvas.drawText(mCharSequence, 0, sequenceLength,
sequenceOffset, rowOffset + mLineHeight,
mForegroundPaint);
}
}
}
}
if(CURSOR_REGION_DEBUG)
if(mGestureMode==GESTURE_MODE_CURSOR) {
mBackgroundPaint.setColor(0x33cccccc);
canvas.translate(0, getScrollY());
for(int i=0; i < mCursorRegions.length; ++i) {
canvas.drawRect(mCursorRegions[i], mBackgroundPaint);
}
}
/* reset back-color */
mBackgroundPaint.setColor(ColorScheme.getColor(mBackColor, false));
}
}
public void onProcessEvent(int event) {
if (mScreenBuffer != null) {
switch (event) {
case TerminalEvent.SCREEN_RESIZE:
mCharSequence = new char[mScreenBuffer.getScreenCols()];
mResizing = false;
post(UpdateRunnable);
mUpdateRequested++;
break;
case TerminalEvent.SCREEN_UPDATE:
/*
* only lines below the screen top can get updated.. check if
* those are in-view if not ignore the update for now
*/
mUpdateRequested++;
break;
}
}
}
private Runnable UpdateRunnable = new Runnable() {
@Override
public void run() {
int delay = 150;
if (mUpdateRequested > 0) {
/* check more frequently */
if (mUpdateRequested > 2)
delay = 100;
mUpdateRequested = 0;
mScreenFillTop = mScreenBuffer.getScreenFillTop();
if (mAutoScrollEnabled)
scrollTo(0, (int) (mScreenFillTop * mLineHeight));
invalidate();
}
/* auto re-check for screen updates every 150ms */
postDelayed(this, delay);
}
};
private void sendBytes(byte[] b) {
sendBytes(b, b.length);
}
private void sendBytes(byte[] b, int length) {
if (mListener != null)
mListener.onEmulatorInput(b, length);
}
public void setEmulatorScreenListener(EmulatorScreenListener listener) {
mListener = listener;
}
public interface EmulatorScreenListener {
public void onEmulatorInput(byte[] b, int length);
public void onAutoScrollChanged(boolean enabled);
public void onGestureModeChanged(int mode);
public void onWindowSizeChangeRequested(int rows, int cols, int width,
int height);
}
}