2015-10-25 14:27:32 +00:00
|
|
|
package com.termux.view;
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint;
|
2016-01-13 02:00:21 +00:00
|
|
|
import android.annotation.TargetApi;
|
|
|
|
import android.content.ClipData;
|
|
|
|
import android.content.ClipboardManager;
|
2015-10-25 14:27:32 +00:00
|
|
|
import android.content.Context;
|
2016-01-13 11:28:31 +00:00
|
|
|
import android.content.res.TypedArray;
|
2015-10-25 14:27:32 +00:00
|
|
|
import android.graphics.Canvas;
|
2016-01-13 02:00:21 +00:00
|
|
|
import android.graphics.Rect;
|
2015-10-25 14:27:32 +00:00
|
|
|
import android.graphics.Typeface;
|
2016-01-13 02:00:21 +00:00
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
2016-01-19 16:21:27 +00:00
|
|
|
import android.media.AudioManager;
|
2016-01-13 02:00:21 +00:00
|
|
|
import android.os.Build;
|
2015-10-25 14:27:32 +00:00
|
|
|
import android.text.InputType;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.AttributeSet;
|
|
|
|
import android.util.Log;
|
2016-01-13 02:00:21 +00:00
|
|
|
import android.view.ActionMode;
|
2015-10-25 14:27:32 +00:00
|
|
|
import android.view.HapticFeedbackConstants;
|
|
|
|
import android.view.InputDevice;
|
|
|
|
import android.view.KeyCharacterMap;
|
|
|
|
import android.view.KeyEvent;
|
2016-01-13 02:00:21 +00:00
|
|
|
import android.view.Menu;
|
|
|
|
import android.view.MenuItem;
|
2015-10-25 14:27:32 +00:00
|
|
|
import android.view.MotionEvent;
|
|
|
|
import android.view.View;
|
|
|
|
import android.view.inputmethod.BaseInputConnection;
|
|
|
|
import android.view.inputmethod.EditorInfo;
|
|
|
|
import android.view.inputmethod.InputConnection;
|
|
|
|
import android.widget.Scroller;
|
|
|
|
|
2016-01-13 02:00:21 +00:00
|
|
|
import com.termux.R;
|
2015-12-28 19:47:34 +00:00
|
|
|
import com.termux.terminal.EmulatorDebug;
|
|
|
|
import com.termux.terminal.KeyHandler;
|
2016-01-13 02:00:21 +00:00
|
|
|
import com.termux.terminal.TerminalBuffer;
|
2015-12-28 19:47:34 +00:00
|
|
|
import com.termux.terminal.TerminalColors;
|
|
|
|
import com.termux.terminal.TerminalEmulator;
|
|
|
|
import com.termux.terminal.TerminalSession;
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.FileInputStream;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.util.Properties;
|
|
|
|
|
2015-10-25 14:27:32 +00:00
|
|
|
/** View displaying and interacting with a {@link TerminalSession}. */
|
|
|
|
public final class TerminalView extends View {
|
|
|
|
|
|
|
|
/** Log view key and IME events. */
|
2016-02-12 23:27:58 +00:00
|
|
|
private static final boolean LOG_KEY_EVENTS = false;
|
2015-10-25 14:27:32 +00:00
|
|
|
|
|
|
|
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
|
|
|
|
TerminalSession mTermSession;
|
|
|
|
/** Our terminal emulator whose session is {@link #mTermSession}. */
|
|
|
|
TerminalEmulator mEmulator;
|
|
|
|
|
|
|
|
TerminalRenderer mRenderer;
|
|
|
|
|
|
|
|
TerminalKeyListener mOnKeyListener;
|
|
|
|
|
|
|
|
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
|
|
|
|
int mTopRow;
|
|
|
|
|
|
|
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
|
|
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
|
|
|
|
2016-01-13 02:00:21 +00:00
|
|
|
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
2015-10-25 14:27:32 +00:00
|
|
|
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
2016-01-13 02:00:21 +00:00
|
|
|
float mSelectionDownX, mSelectionDownY;
|
|
|
|
private ActionMode mActionMode;
|
|
|
|
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
2015-10-25 14:27:32 +00:00
|
|
|
|
|
|
|
float mScaleFactor = 1.f;
|
|
|
|
final GestureAndScaleRecognizer mGestureRecognizer;
|
|
|
|
|
|
|
|
/** Keep track of where mouse touch event started which we report as mouse scroll. */
|
|
|
|
private int mMouseScrollStartX = -1, mMouseScrollStartY = -1;
|
|
|
|
/** Keep track of the time when a touch event leading to sending mouse scroll events started. */
|
|
|
|
private long mMouseStartDownTime = -1;
|
|
|
|
|
|
|
|
final Scroller mScroller;
|
|
|
|
|
|
|
|
/** What was left in from scrolling movement. */
|
|
|
|
float mScrollRemainder;
|
|
|
|
|
|
|
|
/** If non-zero, this is the last unicode code point received if that was a combining character. */
|
|
|
|
int mCombiningAccent;
|
|
|
|
|
|
|
|
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
|
|
|
|
super(context, attributes);
|
|
|
|
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onUp(MotionEvent e) {
|
|
|
|
mScrollRemainder = 0.0f;
|
2016-01-13 02:00:21 +00:00
|
|
|
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) {
|
2015-10-25 14:27:32 +00:00
|
|
|
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
|
|
|
// for zooming.
|
|
|
|
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
|
|
|
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onSingleTapUp(MotionEvent e) {
|
|
|
|
if (mEmulator == null) return true;
|
2016-01-13 02:00:21 +00:00
|
|
|
if (mIsSelectingText) { toggleSelectingText(null); return true; }
|
2015-10-25 14:27:32 +00:00
|
|
|
requestFocus();
|
|
|
|
if (!mEmulator.isMouseTrackingActive()) {
|
|
|
|
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
|
|
|
mOnKeyListener.onSingleTapUp(e);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
|
2016-01-13 02:00:21 +00:00
|
|
|
if (mEmulator == null || mIsSelectingText) return true;
|
2015-10-25 14:27:32 +00:00
|
|
|
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
|
|
|
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
|
|
|
// This means that we never report moving with button press-events for touch input,
|
|
|
|
// since we cannot just start sending these events without a starting press event,
|
|
|
|
// which we do not do for touch input, only mouse in onTouchEvent().
|
|
|
|
sendMouseEventCode(e2, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
|
|
|
} else {
|
|
|
|
distanceY += mScrollRemainder;
|
|
|
|
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
|
|
|
|
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
|
|
|
|
doScroll(e2, deltaRows);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onScale(float focusX, float focusY, float scale) {
|
2016-01-13 09:52:23 +00:00
|
|
|
if (mEmulator == null || mIsSelectingText) return true;
|
2015-10-25 14:27:32 +00:00
|
|
|
mScaleFactor *= scale;
|
|
|
|
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
2016-01-13 02:00:21 +00:00
|
|
|
if (mEmulator == null || mIsSelectingText) return true;
|
2015-10-25 14:27:32 +00:00
|
|
|
// Do not start scrolling until last fling has been taken care of:
|
|
|
|
if (!mScroller.isFinished()) return true;
|
|
|
|
|
|
|
|
final boolean mouseTrackingAtStartOfFling = mEmulator.isMouseTrackingActive();
|
|
|
|
float SCALE = 0.25f;
|
|
|
|
if (mouseTrackingAtStartOfFling) {
|
|
|
|
mScroller.fling(0, 0, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.mRows / 2, mEmulator.mRows / 2);
|
|
|
|
} else {
|
|
|
|
mScroller.fling(0, mTopRow, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.getScreen().getActiveTranscriptRows(), 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
post(new Runnable() {
|
|
|
|
private int mLastY = 0;
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
if (mouseTrackingAtStartOfFling != mEmulator.isMouseTrackingActive()) {
|
|
|
|
mScroller.abortAnimation();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (mScroller.isFinished()) return;
|
|
|
|
boolean more = mScroller.computeScrollOffset();
|
|
|
|
int newY = mScroller.getCurrY();
|
|
|
|
int diff = mouseTrackingAtStartOfFling ? (newY - mLastY) : (newY - mTopRow);
|
|
|
|
doScroll(e2, diff);
|
|
|
|
mLastY = newY;
|
|
|
|
if (more) post(this);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onDown(float x, float y) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onDoubleTap(MotionEvent e) {
|
|
|
|
// Do not treat is as a single confirmed tap - it may be followed by zoom.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onLongPress(MotionEvent e) {
|
2016-01-13 02:42:46 +00:00
|
|
|
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
|
2015-10-25 14:27:32 +00:00
|
|
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
2016-01-13 02:00:21 +00:00
|
|
|
toggleSelectingText(e);
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
mScroller = new Scroller(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param onKeyListener
|
|
|
|
* Listener for all kinds of key events, both hardware and IME (which makes it different from that
|
|
|
|
* available with {@link View#setOnKeyListener(OnKeyListener)}.
|
|
|
|
*/
|
|
|
|
public void setOnKeyListener(TerminalKeyListener onKeyListener) {
|
|
|
|
this.mOnKeyListener = onKeyListener;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attach a {@link TerminalSession} to this view.
|
|
|
|
*
|
|
|
|
* @param session
|
|
|
|
* The {@link TerminalSession} this view will be displaying.
|
|
|
|
*/
|
|
|
|
public boolean attachSession(TerminalSession session) {
|
|
|
|
if (session == mTermSession) return false;
|
|
|
|
mTopRow = 0;
|
|
|
|
|
|
|
|
mTermSession = session;
|
|
|
|
mEmulator = null;
|
|
|
|
mCombiningAccent = 0;
|
|
|
|
|
|
|
|
updateSize();
|
|
|
|
|
|
|
|
// Wait with enabling the scrollbar until we have a terminal to get scroll position from.
|
|
|
|
setVerticalScrollBarEnabled(true);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
|
|
|
// Make the IME run in a limited "generate key events" mode.
|
|
|
|
//
|
|
|
|
// If using just "TYPE_NULL", there is a problem with the "Google Pinyin Input" being in
|
|
|
|
// word mode when used with the "En" tab available when the "Show English keyboard" option
|
|
|
|
// is enabled - see https://github.com/termux/termux-packages/issues/25.
|
|
|
|
//
|
|
|
|
// Adding TYPE_TEXT_FLAG_NO_SUGGESTIONS fixes Pinyin Input, put causes Swype to be put in
|
|
|
|
// word mode... Using TYPE_TEXT_VARIATION_VISIBLE_PASSWORD fixes that.
|
|
|
|
//
|
|
|
|
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
|
|
|
|
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
|
2016-01-13 02:00:21 +00:00
|
|
|
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
|
2015-10-25 14:27:32 +00:00
|
|
|
|
|
|
|
// Let part of the application show behind when in landscape:
|
|
|
|
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
|
|
|
|
|
|
|
return new BaseInputConnection(this, true) {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean beginBatchEdit() {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: beginBatchEdit()");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean endBatchEdit() {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: endBatchEdit()");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-05-09 13:39:11 +00:00
|
|
|
@Override
|
|
|
|
public boolean finishComposingText() {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
|
|
|
|
commitText(getEditable(), 0);
|
|
|
|
return true;
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
|
|
|
|
if (mEmulator == null) return true;
|
|
|
|
final int textLengthInChars = text.length();
|
|
|
|
for (int i = 0; i < textLengthInChars; i++) {
|
|
|
|
char firstChar = text.charAt(i);
|
|
|
|
int codePoint;
|
|
|
|
if (Character.isHighSurrogate(firstChar)) {
|
|
|
|
if (++i < textLengthInChars) {
|
|
|
|
codePoint = Character.toCodePoint(firstChar, text.charAt(i));
|
|
|
|
} else {
|
|
|
|
// At end of string, with no low surrogate following the high:
|
|
|
|
codePoint = TerminalEmulator.UNICODE_REPLACEMENT_CHAR;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
codePoint = firstChar;
|
|
|
|
}
|
|
|
|
inputCodePoint(codePoint, false, false);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
|
|
|
|
|
|
|
|
// Swype keyboard sometimes(?) sends this on backspace:
|
|
|
|
if (leftLength == 0 && rightLength == 0) leftLength = 1;
|
|
|
|
|
|
|
|
for (int i = 0; i < leftLength; i++)
|
|
|
|
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected int computeVerticalScrollRange() {
|
|
|
|
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected int computeVerticalScrollExtent() {
|
|
|
|
return mEmulator == null ? 1 : mEmulator.mRows;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected int computeVerticalScrollOffset() {
|
|
|
|
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onScreenUpdated() {
|
|
|
|
if (mEmulator == null) return;
|
|
|
|
|
2016-01-13 03:20:36 +00:00
|
|
|
boolean skipScrolling = false;
|
2015-10-25 14:27:32 +00:00
|
|
|
if (mIsSelectingText) {
|
2016-01-13 03:20:36 +00:00
|
|
|
// Do not scroll when selecting text.
|
|
|
|
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
2015-10-25 14:27:32 +00:00
|
|
|
int rowShift = mEmulator.getScrollCounter();
|
2016-01-13 03:20:36 +00:00
|
|
|
if (-mTopRow + rowShift > rowsInHistory) {
|
|
|
|
// .. unless we're hitting the end of history transcript, in which
|
|
|
|
// case we abort text selection and scroll to end.
|
|
|
|
toggleSelectingText(null);
|
|
|
|
} else {
|
|
|
|
skipScrolling = true;
|
|
|
|
mTopRow -= rowShift;
|
|
|
|
mSelY1 -= rowShift;
|
|
|
|
mSelY2 -= rowShift;
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
|
2016-01-13 03:20:36 +00:00
|
|
|
if (!skipScrolling && mTopRow != 0) {
|
2015-10-25 14:27:32 +00:00
|
|
|
// Scroll down if not already there.
|
2016-01-13 03:20:36 +00:00
|
|
|
if (mTopRow < -3) {
|
|
|
|
// Awaken scroll bars only if scrolling a noticeable amount
|
|
|
|
// - we do not want visible scroll bars during normal typing
|
|
|
|
// of one row at a time.
|
|
|
|
awakenScrollBars();
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
mTopRow = 0;
|
|
|
|
}
|
|
|
|
|
2016-01-13 03:20:36 +00:00
|
|
|
mEmulator.clearScrollCounter();
|
|
|
|
|
2015-10-25 14:27:32 +00:00
|
|
|
invalidate();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the text size, which in turn sets the number of rows and columns.
|
|
|
|
*
|
|
|
|
* @param textSize
|
|
|
|
* the new font size, in density-independent pixels.
|
|
|
|
*/
|
|
|
|
public void setTextSize(int textSize) {
|
|
|
|
mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface);
|
|
|
|
updateSize();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onCheckIsTextEditor() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isOpaque() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Send a single mouse event code to the terminal. */
|
|
|
|
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
|
|
|
|
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
|
|
|
|
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
|
|
|
|
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
|
|
|
|
if (mMouseStartDownTime == e.getDownTime()) {
|
|
|
|
x = mMouseScrollStartX;
|
|
|
|
y = mMouseScrollStartY;
|
|
|
|
} else {
|
|
|
|
mMouseStartDownTime = e.getDownTime();
|
|
|
|
mMouseScrollStartX = x;
|
|
|
|
mMouseScrollStartY = y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mEmulator.sendMouseEvent(button, x, y, pressed);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Perform a scroll, either from dragging the screen or by scrolling a mouse wheel. */
|
|
|
|
void doScroll(MotionEvent event, int rowsDown) {
|
|
|
|
boolean up = rowsDown < 0;
|
|
|
|
int amount = Math.abs(rowsDown);
|
|
|
|
for (int i = 0; i < amount; i++) {
|
|
|
|
if (mEmulator.isMouseTrackingActive()) {
|
|
|
|
sendMouseEventCode(event, up ? TerminalEmulator.MOUSE_WHEELUP_BUTTON : TerminalEmulator.MOUSE_WHEELDOWN_BUTTON, true);
|
|
|
|
} else if (mEmulator.isAlternateBufferActive()) {
|
|
|
|
// Send up and down key events for scrolling, which is what some terminals do to make scroll work in
|
|
|
|
// e.g. less, which shifts to the alt screen without mouse handling.
|
|
|
|
handleKeyCode(up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN, 0);
|
|
|
|
} else {
|
|
|
|
mTopRow = Math.min(0, Math.max(-(mEmulator.getScreen().getActiveTranscriptRows()), mTopRow + (up ? -1 : 1)));
|
|
|
|
if (!awakenScrollBars()) invalidate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Overriding {@link View#onGenericMotionEvent(MotionEvent)}. */
|
|
|
|
@Override
|
|
|
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
|
|
|
if (mEmulator != null && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) {
|
|
|
|
// Handle mouse wheel scrolling.
|
|
|
|
boolean up = event.getAxisValue(MotionEvent.AXIS_VSCROLL) > 0.0f;
|
|
|
|
doScroll(event, up ? -3 : 3);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
@Override
|
2016-01-13 02:00:21 +00:00
|
|
|
@TargetApi(23)
|
2015-10-25 14:27:32 +00:00
|
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
|
|
if (mEmulator == null) return true;
|
|
|
|
final int action = ev.getAction();
|
|
|
|
|
|
|
|
if (mIsSelectingText) {
|
2016-01-13 02:00:21 +00:00
|
|
|
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
2015-10-25 14:27:32 +00:00
|
|
|
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
2016-01-13 02:00:21 +00:00
|
|
|
|
2015-10-25 14:27:32 +00:00
|
|
|
switch (action) {
|
2016-01-13 02:00:21 +00:00
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
|
mInitialTextSelection = false;
|
|
|
|
break;
|
2015-10-25 14:27:32 +00:00
|
|
|
case MotionEvent.ACTION_DOWN:
|
2016-01-13 02:00:21 +00:00
|
|
|
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
|
|
|
|
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
|
|
|
|
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
|
|
|
|
mSelectionDownX = ev.getX();
|
|
|
|
mSelectionDownY = ev.getY();
|
2015-10-25 14:27:32 +00:00
|
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
2016-01-13 02:00:21 +00:00
|
|
|
if (mInitialTextSelection) break;
|
|
|
|
float deltaX = ev.getX() - mSelectionDownX;
|
|
|
|
float deltaY = ev.getY() - mSelectionDownY;
|
|
|
|
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
|
|
|
|
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
|
|
|
|
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
|
|
|
|
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
|
|
|
|
if (mIsDraggingLeftSelection) {
|
|
|
|
mSelX1 += deltaCols;
|
|
|
|
mSelY1 += deltaRows;
|
|
|
|
} else {
|
|
|
|
mSelX2 += deltaCols;
|
|
|
|
mSelY2 += deltaRows;
|
|
|
|
}
|
|
|
|
|
|
|
|
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
|
|
|
|
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
|
|
|
|
|
|
|
|
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
|
|
|
|
// Switch handles.
|
|
|
|
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
|
|
|
|
int tmpX1 = mSelX1, tmpY1 = mSelY1;
|
|
|
|
mSelX1 = mSelX2; mSelY1 = mSelY2;
|
|
|
|
mSelX2 = tmpX1; mSelY2 = tmpY1;
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
2016-01-13 02:00:21 +00:00
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect();
|
2015-10-25 14:27:32 +00:00
|
|
|
invalidate();
|
2016-01-13 09:52:23 +00:00
|
|
|
break;
|
2015-10-25 14:27:32 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2016-01-13 02:00:21 +00:00
|
|
|
mGestureRecognizer.onTouchEvent(ev);
|
|
|
|
return true;
|
|
|
|
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
|
|
|
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
|
|
|
|
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
|
|
|
return true;
|
|
|
|
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
|
|
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
|
|
|
ClipData clipData = clipboard.getPrimaryClip();
|
|
|
|
if (clipData != null) {
|
|
|
|
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
|
|
|
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
|
|
|
}
|
|
|
|
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
|
|
|
switch (ev.getAction()) {
|
|
|
|
case MotionEvent.ACTION_DOWN:
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
|
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
|
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
|
|
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
|
2016-01-13 12:17:36 +00:00
|
|
|
mGestureRecognizer.onTouchEvent(ev);
|
|
|
|
return true;
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
2016-01-13 02:00:21 +00:00
|
|
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
|
|
if (mIsSelectingText) {
|
|
|
|
toggleSelectingText(null);
|
|
|
|
return true;
|
|
|
|
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
|
|
|
|
// Intercept back button to treat it as escape:
|
|
|
|
switch (event.getAction()) {
|
|
|
|
case KeyEvent.ACTION_DOWN:
|
|
|
|
return onKeyDown(keyCode, event);
|
|
|
|
case KeyEvent.ACTION_UP:
|
|
|
|
return onKeyUp(keyCode, event);
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return super.onKeyPreIme(keyCode, event);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
|
|
|
|
if (mEmulator == null) return true;
|
|
|
|
|
|
|
|
int metaState = event.getMetaState();
|
|
|
|
boolean controlDownFromEvent = event.isCtrlPressed();
|
|
|
|
boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
|
|
|
|
boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
|
|
|
|
|
|
|
if (handleVirtualKeys(keyCode, event, true)) {
|
|
|
|
invalidate();
|
|
|
|
return true;
|
2015-12-23 00:43:41 +00:00
|
|
|
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
|
2015-10-25 14:27:32 +00:00
|
|
|
return super.onKeyDown(keyCode, event);
|
|
|
|
}
|
|
|
|
|
|
|
|
int keyMod = 0;
|
|
|
|
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
|
|
|
|
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
|
|
|
|
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
|
|
|
if (handleKeyCode(keyCode, keyMod)) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear Ctrl since we handle that ourselves:
|
|
|
|
int bitsToClear = KeyEvent.META_CTRL_MASK;
|
|
|
|
if (rightAltDownFromEvent) {
|
|
|
|
// Let right Alt/Alt Gr be used to compose characters.
|
|
|
|
} else {
|
|
|
|
// Use left alt to send to terminal (e.g. Left Alt+B to jump back a word), so remove:
|
|
|
|
bitsToClear |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
|
|
|
}
|
|
|
|
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
|
|
|
|
|
|
|
|
int result = event.getUnicodeChar(effectiveMetaState);
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
|
|
|
|
if (result == 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
int oldCombiningAccent = mCombiningAccent;
|
|
|
|
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
|
|
|
// If entered combining accent previously, write it out:
|
|
|
|
if (mCombiningAccent != 0) inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
|
|
|
|
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
|
|
|
} else {
|
|
|
|
if (mCombiningAccent != 0) {
|
|
|
|
int combinedChar = KeyCharacterMap.getDeadChar(mCombiningAccent, result);
|
|
|
|
if (combinedChar > 0) result = combinedChar;
|
|
|
|
mCombiningAccent = 0;
|
|
|
|
}
|
|
|
|
inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
|
|
|
if (LOG_KEY_EVENTS) {
|
|
|
|
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
|
|
|
+ leftAltDownFromEvent + ")");
|
|
|
|
}
|
|
|
|
|
|
|
|
int resultingKeyCode = -1; // Set if virtual key causes this to be translated to key event.
|
|
|
|
if (controlDownFromEvent || mVirtualControlKeyDown) {
|
|
|
|
if (codePoint >= 'a' && codePoint <= 'z') {
|
|
|
|
codePoint = codePoint - 'a' + 1;
|
|
|
|
} else if (codePoint >= 'A' && codePoint <= 'Z') {
|
|
|
|
codePoint = codePoint - 'A' + 1;
|
|
|
|
} else if (codePoint == ' ' || codePoint == '2') {
|
|
|
|
codePoint = 0;
|
|
|
|
} else if (codePoint == '[' || codePoint == '3') {
|
|
|
|
codePoint = 27; // ^[ (Esc)
|
|
|
|
} else if (codePoint == '\\' || codePoint == '4') {
|
|
|
|
codePoint = 28;
|
|
|
|
} else if (codePoint == ']' || codePoint == '5') {
|
|
|
|
codePoint = 29;
|
|
|
|
} else if (codePoint == '^' || codePoint == '6') {
|
|
|
|
codePoint = 30; // control-^
|
|
|
|
} else if (codePoint == '_' || codePoint == '7') {
|
|
|
|
codePoint = 31;
|
|
|
|
} else if (codePoint == '8') {
|
|
|
|
codePoint = 127; // DEL
|
|
|
|
} else if (codePoint == '9') {
|
|
|
|
resultingKeyCode = KeyEvent.KEYCODE_F11;
|
|
|
|
} else if (codePoint == '0') {
|
|
|
|
resultingKeyCode = KeyEvent.KEYCODE_F12;
|
|
|
|
}
|
|
|
|
} else if (mVirtualFnKeyDown) {
|
2016-04-22 00:20:10 +00:00
|
|
|
int lowerCase = Character.toLowerCase(codePoint);
|
|
|
|
switch (lowerCase) {
|
|
|
|
// Arrow keys.
|
|
|
|
case 'w': resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP; break;
|
|
|
|
case 'a': resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT; break;
|
|
|
|
case 's': resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN; break;
|
|
|
|
case 'd': resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT; break;
|
|
|
|
|
|
|
|
// Page up and down.
|
|
|
|
case 'p': resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP; break;
|
|
|
|
case 'n': resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN; break;
|
|
|
|
|
|
|
|
// Some special keys:
|
|
|
|
case 't': resultingKeyCode = KeyEvent.KEYCODE_TAB; break;
|
|
|
|
case 'i': resultingKeyCode = KeyEvent.KEYCODE_INSERT; break;
|
|
|
|
case 'h': resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME; break;
|
|
|
|
|
|
|
|
// Special characters to input.
|
|
|
|
case 'u': codePoint = '_'; break;
|
|
|
|
case 'l': codePoint = '|'; break;
|
|
|
|
|
|
|
|
// Function keys.
|
|
|
|
case '1': case '2': case '3':
|
|
|
|
case '4': case '5': case '6':
|
|
|
|
case '7': case '8': case '9':
|
|
|
|
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
|
|
|
break;
|
|
|
|
case '0':
|
|
|
|
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Other special keys.
|
|
|
|
case 'e': codePoint = /*Escape*/ 27; break;
|
|
|
|
case '.': codePoint = /*^.*/ 28; break;
|
|
|
|
|
|
|
|
case 'b': // alt+b, jumping backward in readline.
|
|
|
|
case 'f': // alf+f, jumping forward in readline.
|
|
|
|
case 'x': // alt+x, common in emacs.
|
|
|
|
codePoint = lowerCase;
|
|
|
|
leftAltDownFromEvent = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Volume control.
|
|
|
|
case 'v':
|
|
|
|
codePoint = -1;
|
|
|
|
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
|
|
|
|
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
|
|
|
break;
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (codePoint > -1) {
|
|
|
|
if (resultingKeyCode > -1) {
|
|
|
|
handleKeyCode(resultingKeyCode, 0);
|
|
|
|
} else {
|
2015-12-29 23:46:08 +00:00
|
|
|
// Work around bluetooth keyboards sending funny unicode characters instead
|
|
|
|
// of the more normal ones from ASCII that terminal programs expect - the
|
|
|
|
// desire to input the original characters should be low.
|
|
|
|
switch (codePoint) {
|
|
|
|
case 0x02DC: // SMALL TILDE.
|
|
|
|
codePoint = 0x007E; // TILDE (~).
|
|
|
|
break;
|
|
|
|
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
|
|
|
codePoint = 0x0060; // GRAVE ACCENT (`).
|
|
|
|
break;
|
|
|
|
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
|
|
|
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
|
|
|
break;
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
|
|
|
|
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
|
|
|
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Input the specified keyCode if applicable and return if the input was consumed. */
|
|
|
|
public boolean handleKeyCode(int keyCode, int keyMod) {
|
|
|
|
TerminalEmulator term = mTermSession.getEmulator();
|
|
|
|
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
|
|
|
|
if (code == null) return false;
|
|
|
|
mTermSession.write(code);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when a key is released in the view.
|
|
|
|
*
|
|
|
|
* @param keyCode
|
|
|
|
* The keycode of the key which was released.
|
|
|
|
* @param event
|
|
|
|
* A {@link KeyEvent} describing the event.
|
|
|
|
* @return Whether the event was handled.
|
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
|
|
|
if (mEmulator == null) return true;
|
|
|
|
|
|
|
|
if (handleVirtualKeys(keyCode, event, false)) {
|
|
|
|
invalidate();
|
|
|
|
return true;
|
|
|
|
} else if (event.isSystem()) {
|
|
|
|
// Let system key events through.
|
|
|
|
return super.onKeyUp(keyCode, event);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
|
|
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
|
|
|
InputDevice inputDevice = event.getDevice();
|
|
|
|
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
|
|
|
// Do not steal dedicated buttons from a full external keyboard.
|
|
|
|
return false;
|
|
|
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking ctrl event");
|
|
|
|
mVirtualControlKeyDown = down;
|
|
|
|
return true;
|
|
|
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
|
|
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking Fn event");
|
|
|
|
mVirtualFnKeyDown = down;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-12-28 19:47:34 +00:00
|
|
|
public void checkForFontAndColors() {
|
|
|
|
try {
|
2016-03-17 11:29:30 +00:00
|
|
|
// Hard-coded paths since this file is used also in Termux:Float.
|
|
|
|
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
|
|
|
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
2015-10-25 14:27:32 +00:00
|
|
|
|
2015-12-28 19:47:34 +00:00
|
|
|
final Properties props = new Properties();
|
|
|
|
if (colorsFile.isFile()) {
|
|
|
|
try (InputStream in = new FileInputStream(colorsFile)) {
|
|
|
|
props.load(in);
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
}
|
2015-12-28 19:47:34 +00:00
|
|
|
TerminalColors.COLOR_SCHEME.updateWith(props);
|
|
|
|
if (mEmulator != null) mEmulator.mColors.reset();
|
|
|
|
|
|
|
|
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
|
|
|
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
|
|
|
updateSize();
|
|
|
|
|
|
|
|
invalidate();
|
|
|
|
} catch (Exception e) {
|
2015-12-28 19:49:46 +00:00
|
|
|
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
|
2015-12-28 19:47:34 +00:00
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is called during layout when the size of this view has changed. If you were just added to the view
|
|
|
|
* hierarchy, you're called with the old values of 0.
|
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
|
|
updateSize();
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Check if the terminal size in rows and columns should be updated. */
|
|
|
|
public void updateSize() {
|
|
|
|
int viewWidth = getWidth();
|
|
|
|
int viewHeight = getHeight();
|
|
|
|
if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return;
|
|
|
|
|
|
|
|
// Set to 80 and 24 if you want to enable vttest.
|
2016-05-04 20:44:38 +00:00
|
|
|
int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth));
|
|
|
|
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
|
2015-10-25 14:27:32 +00:00
|
|
|
|
|
|
|
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
|
|
|
mTermSession.updateSize(newColumns, newRows);
|
|
|
|
mEmulator = mTermSession.getEmulator();
|
|
|
|
|
|
|
|
mTopRow = 0;
|
|
|
|
scrollTo(0, 0);
|
|
|
|
invalidate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onDraw(Canvas canvas) {
|
|
|
|
if (mEmulator == null) {
|
|
|
|
canvas.drawColor(0XFF000000);
|
|
|
|
} else {
|
|
|
|
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
2016-01-13 02:00:21 +00:00
|
|
|
|
|
|
|
if (mIsSelectingText) {
|
|
|
|
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
|
|
|
|
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
|
|
|
|
|
|
|
|
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
|
|
|
|
int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
|
|
|
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
|
|
|
|
mLeftSelectionHandle.draw(canvas);
|
|
|
|
|
|
|
|
int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin;
|
|
|
|
top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
|
|
|
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
|
|
|
|
mRightSelectionHandle.draw(canvas);
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Toggle text selection mode in the view. */
|
2016-01-13 02:00:21 +00:00
|
|
|
@TargetApi(23)
|
|
|
|
public void toggleSelectingText(MotionEvent ev) {
|
2015-10-25 14:27:32 +00:00
|
|
|
mIsSelectingText = !mIsSelectingText;
|
2016-01-13 02:00:21 +00:00
|
|
|
mOnKeyListener.copyModeChanged(mIsSelectingText);
|
|
|
|
|
|
|
|
if (mIsSelectingText) {
|
|
|
|
if (mLeftSelectionHandle == null) {
|
|
|
|
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
|
|
|
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
|
|
|
}
|
|
|
|
|
|
|
|
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
|
|
|
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
|
|
|
// Offset for finger:
|
|
|
|
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
|
|
|
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
|
|
|
|
|
|
|
mSelX1 = mSelX2 = cx;
|
|
|
|
mSelY1 = mSelY2 = cy;
|
|
|
|
|
|
|
|
TerminalBuffer screen = mEmulator.getScreen();
|
|
|
|
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
|
|
|
// Selecting something other than whitespace. Expand to word.
|
|
|
|
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) {
|
|
|
|
mSelX1--;
|
|
|
|
}
|
|
|
|
while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) {
|
|
|
|
mSelX2++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mInitialTextSelection = true;
|
|
|
|
mIsDraggingLeftSelection = true;
|
|
|
|
mSelectionDownX = ev.getX();
|
|
|
|
mSelectionDownY = ev.getY();
|
|
|
|
|
|
|
|
final ActionMode.Callback callback = new ActionMode.Callback() {
|
|
|
|
@Override
|
|
|
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
2016-01-13 11:28:31 +00:00
|
|
|
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
|
|
|
|
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
|
2016-03-17 11:29:30 +00:00
|
|
|
try {
|
|
|
|
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
|
|
|
|
|
|
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
|
|
|
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
|
|
|
|
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
|
|
|
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
|
|
|
} finally {
|
|
|
|
styledAttributes.recycle();
|
|
|
|
}
|
2016-01-13 02:00:21 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
|
|
|
switch (item.getItemId()) {
|
|
|
|
case 1:
|
|
|
|
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
|
|
|
mTermSession.clipboardText(selectedText);
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
|
|
|
ClipData clipData = clipboard.getPrimaryClip();
|
|
|
|
if (clipData != null) {
|
|
|
|
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
|
|
|
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
showContextMenu();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
toggleSelectingText(null);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onDestroyActionMode(ActionMode mode) {
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
|
mActionMode = startActionMode(new ActionMode.Callback2() {
|
|
|
|
@Override
|
|
|
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
|
|
|
return callback.onCreateActionMode(mode, menu);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
|
|
|
return callback.onActionItemClicked(mode, item);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onDestroyActionMode(ActionMode mode) {
|
|
|
|
// Ignore.
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
|
|
|
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
|
|
|
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
|
|
|
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
|
|
|
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
|
|
|
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
|
|
|
|
}
|
|
|
|
}, ActionMode.TYPE_FLOATING);
|
|
|
|
} else {
|
|
|
|
mActionMode = startActionMode(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
invalidate();
|
|
|
|
} else {
|
|
|
|
mActionMode.finish();
|
|
|
|
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
|
|
|
invalidate();
|
|
|
|
}
|
2015-10-25 14:27:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public TerminalSession getCurrentSession() {
|
|
|
|
return mTermSession;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|