mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-04 17:55:36 +08:00
Restructure library modules
terminal/ -> terminal-emulator/ view/ -> terminal-view/
This commit is contained in:
28
terminal-view/build.gradle
Normal file
28
terminal-view/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.3.1'
|
||||
compile project(":terminal-emulator")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
25
terminal-view/proguard-rules.pro
vendored
Normal file
25
terminal-view/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
3
terminal-view/src/main/AndroidManifest.xml
Normal file
3
terminal-view/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.view">
|
||||
</manifest>
|
@@ -0,0 +1,111 @@
|
||||
package com.termux.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
|
||||
public final class GestureAndScaleRecognizer {
|
||||
|
||||
public interface Listener {
|
||||
boolean onSingleTapUp(MotionEvent e);
|
||||
|
||||
boolean onDoubleTap(MotionEvent e);
|
||||
|
||||
boolean onScroll(MotionEvent e2, float dx, float dy);
|
||||
|
||||
boolean onFling(MotionEvent e, float velocityX, float velocityY);
|
||||
|
||||
boolean onScale(float focusX, float focusY, float scale);
|
||||
|
||||
boolean onDown(float x, float y);
|
||||
|
||||
boolean onUp(MotionEvent e);
|
||||
|
||||
void onLongPress(MotionEvent e);
|
||||
}
|
||||
|
||||
private final GestureDetector mGestureDetector;
|
||||
private final ScaleGestureDetector mScaleDetector;
|
||||
final Listener mListener;
|
||||
boolean isAfterLongPress;
|
||||
|
||||
public GestureAndScaleRecognizer(Context context, Listener listener) {
|
||||
mListener = listener;
|
||||
|
||||
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
|
||||
return mListener.onScroll(e2, dx, dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return mListener.onFling(e2, velocityX, velocityY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
return mListener.onDown(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
mListener.onLongPress(e);
|
||||
isAfterLongPress = true;
|
||||
}
|
||||
}, null, true /* ignoreMultitouch */);
|
||||
|
||||
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
return mListener.onSingleTapUp(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
return mListener.onDoubleTap(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onTouchEvent(MotionEvent event) {
|
||||
mGestureDetector.onTouchEvent(event);
|
||||
mScaleDetector.onTouchEvent(event);
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
isAfterLongPress = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!isAfterLongPress) {
|
||||
// This behaviour is desired when in e.g. vim with mouse events, where we do not
|
||||
// want to move the cursor when lifting finger after a long press.
|
||||
mListener.onUp(event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return mScaleDetector.isInProgress();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package com.termux.view;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
/**
|
||||
* Input and scale listener which may be set on a {@link TerminalView} through
|
||||
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
|
||||
* <p/>
|
||||
* TODO: Rename to TerminalViewClient.
|
||||
*/
|
||||
public interface TerminalKeyListener {
|
||||
|
||||
/**
|
||||
* Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}.
|
||||
*/
|
||||
float onScale(float scale);
|
||||
|
||||
/**
|
||||
* On a single tap on the terminal if terminal mouse reporting not enabled.
|
||||
*/
|
||||
void onSingleTapUp(MotionEvent e);
|
||||
|
||||
boolean shouldBackButtonBeMappedToEscape();
|
||||
|
||||
void copyModeChanged(boolean copyMode);
|
||||
|
||||
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);
|
||||
|
||||
boolean onKeyUp(int keyCode, KeyEvent e);
|
||||
|
||||
boolean readControlKey();
|
||||
|
||||
boolean readAltKey();
|
||||
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
boolean onLongPress(MotionEvent event);
|
||||
|
||||
}
|
@@ -0,0 +1,230 @@
|
||||
package com.termux.view;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalRow;
|
||||
import com.termux.terminal.TextStyle;
|
||||
import com.termux.terminal.WcWidth;
|
||||
|
||||
/**
|
||||
* Renderer of a {@link TerminalEmulator} into a {@link Canvas}.
|
||||
* <p/>
|
||||
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||
*/
|
||||
final class TerminalRenderer {
|
||||
|
||||
final int mTextSize;
|
||||
final Typeface mTypeface;
|
||||
private final Paint mTextPaint = new Paint();
|
||||
|
||||
/** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */
|
||||
final float mFontWidth;
|
||||
/** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
final int mFontLineSpacing;
|
||||
/** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
private final int mFontAscent;
|
||||
/** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */
|
||||
final int mFontLineSpacingAndAscent;
|
||||
|
||||
private final float[] asciiMeasures = new float[127];
|
||||
|
||||
public TerminalRenderer(int textSize, Typeface typeface) {
|
||||
mTextSize = textSize;
|
||||
mTypeface = typeface;
|
||||
|
||||
mTextPaint.setTypeface(typeface);
|
||||
mTextPaint.setAntiAlias(true);
|
||||
mTextPaint.setTextSize(textSize);
|
||||
|
||||
mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing());
|
||||
mFontAscent = (int) Math.ceil(mTextPaint.ascent());
|
||||
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent;
|
||||
mFontWidth = mTextPaint.measureText("X");
|
||||
|
||||
StringBuilder sb = new StringBuilder(" ");
|
||||
for (int i = 0; i < asciiMeasures.length; i++) {
|
||||
sb.setCharAt(0, (char) i);
|
||||
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
|
||||
public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow,
|
||||
int selectionY1, int selectionY2, int selectionX1, int selectionX2) {
|
||||
final boolean reverseVideo = mEmulator.isReverseVideo();
|
||||
final int endRow = topRow + mEmulator.mRows;
|
||||
final int columns = mEmulator.mColumns;
|
||||
final int cursorCol = mEmulator.getCursorCol();
|
||||
final int cursorRow = mEmulator.getCursorRow();
|
||||
final boolean cursorVisible = mEmulator.isShowingCursor();
|
||||
final TerminalBuffer screen = mEmulator.getScreen();
|
||||
final int[] palette = mEmulator.mColors.mCurrentColors;
|
||||
final int cursorShape = mEmulator.getCursorStyle();
|
||||
|
||||
if (reverseVideo)
|
||||
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC);
|
||||
|
||||
float heightOffset = mFontLineSpacingAndAscent;
|
||||
for (int row = topRow; row < endRow; row++) {
|
||||
heightOffset += mFontLineSpacing;
|
||||
|
||||
final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1;
|
||||
int selx1 = -1, selx2 = -1;
|
||||
if (row >= selectionY1 && row <= selectionY2) {
|
||||
if (row == selectionY1) selx1 = selectionX1;
|
||||
selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns;
|
||||
}
|
||||
|
||||
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
|
||||
final char[] line = lineObject.mText;
|
||||
final int charsUsedInLine = lineObject.getSpaceUsed();
|
||||
|
||||
long lastRunStyle = 0;
|
||||
boolean lastRunInsideCursor = false;
|
||||
int lastRunStartColumn = -1;
|
||||
int lastRunStartIndex = 0;
|
||||
boolean lastRunFontWidthMismatch = false;
|
||||
int currentCharIndex = 0;
|
||||
float measuredWidthForRun = 0.f;
|
||||
|
||||
for (int column = 0; column < columns; ) {
|
||||
final char charAtIndex = line[currentCharIndex];
|
||||
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
|
||||
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
|
||||
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
|
||||
final int codePointWcWidth = WcWidth.width(codePoint);
|
||||
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
|
||||
final long style = lineObject.getStyle(column);
|
||||
|
||||
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
|
||||
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
|
||||
// smileys which android font renders as wide.
|
||||
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
|
||||
final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line,
|
||||
currentCharIndex, charsForCodePoint);
|
||||
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
|
||||
|
||||
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) {
|
||||
if (column == 0) {
|
||||
// Skip first column as there is nothing to draw, just record the current style.
|
||||
} else {
|
||||
final int columnWidthSinceLastRun = column - lastRunStartColumn;
|
||||
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle, reverseVideo);
|
||||
}
|
||||
measuredWidthForRun = 0.f;
|
||||
lastRunStyle = style;
|
||||
lastRunInsideCursor = insideCursor;
|
||||
lastRunStartColumn = column;
|
||||
lastRunStartIndex = currentCharIndex;
|
||||
lastRunFontWidthMismatch = fontWidthMismatch;
|
||||
}
|
||||
measuredWidthForRun += measuredCodePointWidth;
|
||||
column += codePointWcWidth;
|
||||
currentCharIndex += charsForCodePoint;
|
||||
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
|
||||
// Eat combining chars so that they are treated as part of the last non-combining code point,
|
||||
// instead of e.g. being considered inside the cursor in the next run.
|
||||
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
|
||||
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
||||
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns,
|
||||
int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle,
|
||||
long textStyle, boolean reverseVideo) {
|
||||
int foreColor = TextStyle.decodeForeColor(textStyle);
|
||||
final int effect = TextStyle.decodeEffect(textStyle);
|
||||
int backColor = TextStyle.decodeBackColor(textStyle);
|
||||
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
|
||||
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
|
||||
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
|
||||
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
|
||||
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
|
||||
|
||||
if ((foreColor & 0xff000000) != 0xff000000) {
|
||||
// Let bold have bright colors if applicable (one of the first 8):
|
||||
if (bold && foreColor >= 0 && foreColor < 8) foreColor += 8;
|
||||
foreColor = palette[foreColor];
|
||||
}
|
||||
|
||||
if ((backColor & 0xff000000) != 0xff000000) {
|
||||
backColor = palette[backColor];
|
||||
}
|
||||
|
||||
// Reverse video here if _one and only one_ of the reverse flags are set:
|
||||
final boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
|
||||
if (reverseVideoHere) {
|
||||
int tmp = foreColor;
|
||||
foreColor = backColor;
|
||||
backColor = tmp;
|
||||
}
|
||||
|
||||
float left = startColumn * mFontWidth;
|
||||
float right = left + runWidthColumns * mFontWidth;
|
||||
|
||||
mes = mes / mFontWidth;
|
||||
boolean savedMatrix = false;
|
||||
if (Math.abs(mes - runWidthColumns) > 0.01) {
|
||||
canvas.save();
|
||||
canvas.scale(runWidthColumns / mes, 1.f);
|
||||
left *= mes / runWidthColumns;
|
||||
right *= mes / runWidthColumns;
|
||||
savedMatrix = true;
|
||||
}
|
||||
|
||||
if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
|
||||
// Only draw non-default background.
|
||||
mTextPaint.setColor(backColor);
|
||||
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
|
||||
}
|
||||
|
||||
if (cursor != 0) {
|
||||
mTextPaint.setColor(cursor);
|
||||
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
|
||||
if (cursorStyle == TerminalEmulator.CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
|
||||
else if (cursorStyle == TerminalEmulator.CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
|
||||
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint);
|
||||
}
|
||||
|
||||
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
||||
if (dim) {
|
||||
int red = (0xFF & (foreColor >> 16));
|
||||
int green = (0xFF & (foreColor >> 8));
|
||||
int blue = (0xFF & foreColor);
|
||||
// Dim color handling used by libvte which in turn took it from xterm
|
||||
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
|
||||
red = red * 2 / 3;
|
||||
green = green * 2 / 3;
|
||||
blue = blue * 2 / 3;
|
||||
foreColor = 0xFF000000 + (red << 16) + (green << 8) + blue;
|
||||
}
|
||||
|
||||
mTextPaint.setFakeBoldText(bold);
|
||||
mTextPaint.setUnderlineText(underline);
|
||||
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
|
||||
mTextPaint.setStrikeThruText(strikeThrough);
|
||||
mTextPaint.setColor(foreColor);
|
||||
|
||||
// The text alignment is the default Paint.Align.LEFT.
|
||||
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore();
|
||||
}
|
||||
}
|
912
terminal-view/src/main/java/com/termux/view/TerminalView.java
Normal file
912
terminal-view/src/main/java/com/termux/view/TerminalView.java
Normal file
@@ -0,0 +1,912 @@
|
||||
package com.termux.view;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
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;
|
||||
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
/** View displaying and interacting with a {@link TerminalSession}. */
|
||||
public final class TerminalView extends View {
|
||||
|
||||
/** Log view key and IME events. */
|
||||
private static final boolean LOG_KEY_EVENTS = false;
|
||||
|
||||
/** 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;
|
||||
|
||||
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
||||
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
float mSelectionDownX, mSelectionDownY;
|
||||
private ActionMode mActionMode;
|
||||
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
||||
|
||||
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() {
|
||||
|
||||
boolean scrolledWithFinger;
|
||||
|
||||
@Override
|
||||
public boolean onUp(MotionEvent e) {
|
||||
mScrollRemainder = 0.0f;
|
||||
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText && !scrolledWithFinger) {
|
||||
// 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;
|
||||
}
|
||||
scrolledWithFinger = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mIsSelectingText) {
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
}
|
||||
requestFocus();
|
||||
if (!mEmulator.isMouseTrackingActive()) {
|
||||
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
mOnKeyListener.onSingleTapUp(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
if (mEmulator.isMouseTrackingActive() && e.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(e, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||
} else {
|
||||
scrolledWithFinger = true;
|
||||
distanceY += mScrollRemainder;
|
||||
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
|
||||
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
|
||||
doScroll(e, deltaRows);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(float focusX, float focusY, float scale) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
mScaleFactor *= scale;
|
||||
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
// 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) {
|
||||
if (mGestureRecognizer.isInProgress()) return;
|
||||
if (mOnKeyListener.onLongPress(e)) return;
|
||||
if (!mIsSelectingText) {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
toggleSelectingText(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
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) {
|
||||
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
|
||||
//
|
||||
// Previous keyboard issues:
|
||||
// https://github.com/termux/termux-packages/issues/25
|
||||
// https://github.com/termux/termux-app/issues/87.
|
||||
// https://github.com/termux/termux-app/issues/126.
|
||||
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
|
||||
outAttrs.inputType = InputType.TYPE_NULL;
|
||||
|
||||
// Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen
|
||||
// keyboard on Android TV (see https://github.com/termux/termux-app/issues/221).
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
|
||||
return new BaseInputConnection(this, true) {
|
||||
|
||||
@Override
|
||||
public boolean finishComposingText() {
|
||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
|
||||
super.finishComposingText();
|
||||
|
||||
sendTextToTerminal(getEditable());
|
||||
getEditable().clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (LOG_KEY_EVENTS) {
|
||||
Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
|
||||
}
|
||||
super.commitText(text, newCursorPosition);
|
||||
|
||||
if (mEmulator == null) return true;
|
||||
|
||||
Editable content = getEditable();
|
||||
sendTextToTerminal(content);
|
||||
content.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
||||
if (LOG_KEY_EVENTS) {
|
||||
Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
|
||||
}
|
||||
// The stock Samsung keyboard with 'Auto check spelling' enabled sends leftLength > 1.
|
||||
KeyEvent deleteKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
||||
for (int i = 0; i < leftLength; i++) sendKeyEvent(deleteKey);
|
||||
return super.deleteSurroundingText(leftLength, rightLength);
|
||||
}
|
||||
|
||||
void sendTextToTerminal(CharSequence text) {
|
||||
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;
|
||||
}
|
||||
|
||||
boolean ctrlHeld = false;
|
||||
if (codePoint <= 31 && codePoint != 27) {
|
||||
if (codePoint == '\n') {
|
||||
// The AOSP keyboard and descendants seems to send \n as text when the enter key is pressed,
|
||||
// instead of a key event like most other keyboard apps. A terminal expects \r for the enter
|
||||
// key (although when icrnl is enabled this doesn't make a difference - run 'stty -icrnl' to
|
||||
// check the behaviour).
|
||||
codePoint = '\r';
|
||||
}
|
||||
|
||||
// E.g. penti keyboard for ctrl input.
|
||||
ctrlHeld = true;
|
||||
switch (codePoint) {
|
||||
case 31:
|
||||
codePoint = '_';
|
||||
break;
|
||||
case 30:
|
||||
codePoint = '^';
|
||||
break;
|
||||
case 29:
|
||||
codePoint = ']';
|
||||
break;
|
||||
case 28:
|
||||
codePoint = '\\';
|
||||
break;
|
||||
default:
|
||||
codePoint += 96;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inputCodePoint(codePoint, ctrlHeld, false);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
boolean skipScrolling = false;
|
||||
if (mIsSelectingText) {
|
||||
// Do not scroll when selecting text.
|
||||
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||
int rowShift = mEmulator.getScrollCounter();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipScrolling && mTopRow != 0) {
|
||||
// Scroll down if not already there.
|
||||
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();
|
||||
}
|
||||
mTopRow = 0;
|
||||
}
|
||||
|
||||
mEmulator.clearScrollCounter();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void setTypeface(Typeface newTypeface) {
|
||||
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
||||
updateSize();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@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
|
||||
@TargetApi(23)
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mEmulator == null) return true;
|
||||
final int action = ev.getAction();
|
||||
|
||||
if (mIsSelectingText) {
|
||||
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
mInitialTextSelection = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
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();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
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;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
mActionMode.invalidateContentRect();
|
||||
invalidate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
if (mOnKeyListener.onKeyDown(keyCode, event, mTermSession)) {
|
||||
invalidate();
|
||||
return true;
|
||||
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
mTermSession.write(event.getCharacters());
|
||||
return true;
|
||||
}
|
||||
|
||||
final int metaState = event.getMetaState();
|
||||
final boolean controlDownFromEvent = event.isCtrlPressed();
|
||||
final boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
|
||||
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
||||
|
||||
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 + ")");
|
||||
}
|
||||
|
||||
final boolean controlDown = controlDownFromEvent || mOnKeyListener.readControlKey();
|
||||
final boolean altDown = leftAltDownFromEvent || mOnKeyListener.readAltKey();
|
||||
|
||||
if (mOnKeyListener.onCodePoint(codePoint, controlDown, mTermSession)) return;
|
||||
|
||||
if (controlDown) {
|
||||
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 == '/') {
|
||||
// "Ctrl-/ sends 0x1f which is equivalent of Ctrl-_ since the days of VT102"
|
||||
// - http://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||
codePoint = 31;
|
||||
} else if (codePoint == '8') {
|
||||
codePoint = 127; // DEL
|
||||
}
|
||||
}
|
||||
|
||||
if (codePoint > -1) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
||||
mTermSession.writeCodePoint(altDown, 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 (mOnKeyListener.onKeyUp(keyCode, event)) {
|
||||
invalidate();
|
||||
return true;
|
||||
} else if (event.isSystem()) {
|
||||
// Let system key events through.
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth));
|
||||
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle text selection mode in the view. */
|
||||
@TargetApi(23)
|
||||
public void toggleSelectingText(MotionEvent ev) {
|
||||
mIsSelectingText = !mIsSelectingText;
|
||||
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) {
|
||||
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).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalSession getCurrentSession() {
|
||||
return mTermSession;
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
5
terminal-view/src/main/res/values/strings.xml
Normal file
5
terminal-view/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<resources>
|
||||
<string name="paste_text">Paste</string>
|
||||
<string name="copy_text">Copy</string>
|
||||
<string name="text_selection_more">More…</string>
|
||||
</resources>
|
Reference in New Issue
Block a user