Repo created
This commit is contained in:
parent
d22b8dc57b
commit
24b567c524
271 changed files with 39630 additions and 2 deletions
53
terminal-view/build.gradle
Normal file
53
terminal-view/build.gradle
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
api project(":terminal-emulator")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-view'
|
||||
version = '0.118.0'
|
||||
artifact(sourceJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
2
terminal-view/src/main/AndroidManifest.xml
Normal file
2
terminal-view/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<manifest package="com.termux.view">
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
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}. */
|
||||
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());
|
||||
}
|
||||
});
|
||||
mScaleDetector.setQuickScaleEnabled(false);
|
||||
}
|
||||
|
||||
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,249 @@
|
|||
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.
|
||||
*/
|
||||
public 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.shouldCursorBeVisible();
|
||||
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;
|
||||
boolean lastRunInsideSelection = 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 = (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
|
||||
final boolean insideSelection = column >= selx1 && column <= selx2;
|
||||
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 || insideSelection != lastRunInsideSelection || 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;
|
||||
boolean invertCursorTextColor = false;
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true;
|
||||
}
|
||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
|
||||
}
|
||||
measuredWidthForRun = 0.f;
|
||||
lastRunStyle = style;
|
||||
lastRunInsideCursor = insideCursor;
|
||||
lastRunInsideSelection = insideSelection;
|
||||
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;
|
||||
boolean invertCursorTextColor = false;
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true;
|
||||
}
|
||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
||||
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
|
||||
}
|
||||
}
|
||||
|
||||
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.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
|
||||
else if (cursorStyle == TerminalEmulator.TERMINAL_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.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint);
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore();
|
||||
}
|
||||
|
||||
public float getFontWidth() {
|
||||
return mFontWidth;
|
||||
}
|
||||
|
||||
public int getFontLineSpacing() {
|
||||
return mFontLineSpacing;
|
||||
}
|
||||
}
|
||||
1476
terminal-view/src/main/java/com/termux/view/TerminalView.java
Normal file
1476
terminal-view/src/main/java/com/termux/view/TerminalView.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,83 @@
|
|||
package com.termux.view;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
/**
|
||||
* The interface for communication between {@link TerminalView} and its client. It allows for getting
|
||||
* various configuration options from the client and for sending back data to the client like logs,
|
||||
* key events, both hardware and IME (which makes it different from that available with
|
||||
* {@link View#setOnKeyListener(View.OnKeyListener)}, etc. It must be set for the
|
||||
* {@link TerminalView} through {@link TerminalView#setTerminalViewClient(TerminalViewClient)}.
|
||||
*/
|
||||
public interface TerminalViewClient {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
boolean shouldEnforceCharBasedInput();
|
||||
|
||||
boolean shouldUseCtrlSpaceWorkaround();
|
||||
|
||||
boolean isTerminalViewSelected();
|
||||
|
||||
|
||||
|
||||
void copyModeChanged(boolean copyMode);
|
||||
|
||||
|
||||
|
||||
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);
|
||||
|
||||
boolean onKeyUp(int keyCode, KeyEvent e);
|
||||
|
||||
boolean onLongPress(MotionEvent event);
|
||||
|
||||
|
||||
|
||||
boolean readControlKey();
|
||||
|
||||
boolean readAltKey();
|
||||
|
||||
boolean readShiftKey();
|
||||
|
||||
boolean readFnKey();
|
||||
|
||||
|
||||
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
|
||||
void onEmulatorSet();
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
void logWarn(String tag, String message);
|
||||
|
||||
void logInfo(String tag, String message);
|
||||
|
||||
void logDebug(String tag, String message);
|
||||
|
||||
void logVerbose(String tag, String message);
|
||||
|
||||
void logStackTraceWithMessage(String tag, String message, Exception e);
|
||||
|
||||
void logStackTrace(String tag, Exception e);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.termux.view.textselection;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
/**
|
||||
* A CursorController instance can be used to control cursors in the text.
|
||||
* It is not used outside of {@link TerminalView}.
|
||||
*/
|
||||
public interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
|
||||
/**
|
||||
* Show the cursors on screen. Will be drawn by {@link #render()} by a call during onDraw.
|
||||
* See also {@link #hide()}.
|
||||
*/
|
||||
void show(MotionEvent event);
|
||||
|
||||
/**
|
||||
* Hide the cursors from screen.
|
||||
* See also {@link #show(MotionEvent event)}.
|
||||
*/
|
||||
boolean hide();
|
||||
|
||||
/**
|
||||
* Render the cursors.
|
||||
*/
|
||||
void render();
|
||||
|
||||
/**
|
||||
* Update the cursor positions.
|
||||
*/
|
||||
void updatePosition(TextSelectionHandleView handle, int x, int y);
|
||||
|
||||
/**
|
||||
* This method is called by {@link #onTouchEvent(MotionEvent)} and gives the cursors
|
||||
* a chance to become active and/or visible.
|
||||
*
|
||||
* @param event The touch event
|
||||
*/
|
||||
boolean onTouchEvent(MotionEvent event);
|
||||
|
||||
/**
|
||||
* Called when the view is detached from window. Perform house keeping task, such as
|
||||
* stopping Runnable thread that would otherwise keep a reference on the context, thus
|
||||
* preventing the activity to be recycled.
|
||||
*/
|
||||
void onDetached();
|
||||
|
||||
/**
|
||||
* @return true if the cursors are currently active.
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,402 @@
|
|||
package com.termux.view.textselection;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ActionMode;
|
||||
import android.view.InputDevice;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.WcWidth;
|
||||
import com.termux.view.R;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
public class TextSelectionCursorController implements CursorController {
|
||||
|
||||
private final TerminalView terminalView;
|
||||
private final TextSelectionHandleView mStartHandle, mEndHandle;
|
||||
private String mStoredSelectedText;
|
||||
private boolean mIsSelectingText = false;
|
||||
private long mShowStartTime = System.currentTimeMillis();
|
||||
|
||||
private final int mHandleHeight;
|
||||
private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
|
||||
private ActionMode mActionMode;
|
||||
public final int ACTION_COPY = 1;
|
||||
public final int ACTION_PASTE = 2;
|
||||
public final int ACTION_MORE = 3;
|
||||
|
||||
public TextSelectionCursorController(TerminalView terminalView) {
|
||||
this.terminalView = terminalView;
|
||||
mStartHandle = new TextSelectionHandleView(terminalView, this, TextSelectionHandleView.LEFT);
|
||||
mEndHandle = new TextSelectionHandleView(terminalView, this, TextSelectionHandleView.RIGHT);
|
||||
|
||||
mHandleHeight = Math.max(mStartHandle.getHandleHeight(), mEndHandle.getHandleHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(MotionEvent event) {
|
||||
setInitialTextSelectionPosition(event);
|
||||
mStartHandle.positionAtCursor(mSelX1, mSelY1, true);
|
||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true);
|
||||
|
||||
setActionModeCallBacks();
|
||||
mShowStartTime = System.currentTimeMillis();
|
||||
mIsSelectingText = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hide() {
|
||||
if (!isActive()) return false;
|
||||
|
||||
// prevent hide calls right after a show call, like long pressing the down key
|
||||
// 300ms seems long enough that it wouldn't cause hide problems if action button
|
||||
// is quickly clicked after the show, otherwise decrease it
|
||||
if (System.currentTimeMillis() - mShowStartTime < 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mStartHandle.hide();
|
||||
mEndHandle.hide();
|
||||
|
||||
if (mActionMode != null) {
|
||||
// This will hide the TextSelectionCursorController
|
||||
mActionMode.finish();
|
||||
}
|
||||
|
||||
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
mIsSelectingText = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render() {
|
||||
if (!isActive()) return;
|
||||
|
||||
mStartHandle.positionAtCursor(mSelX1, mSelY1, false);
|
||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false);
|
||||
|
||||
if (mActionMode != null) {
|
||||
mActionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitialTextSelectionPosition(MotionEvent event) {
|
||||
int[] columnAndRow = terminalView.getColumnAndRow(event, true);
|
||||
mSelX1 = mSelX2 = columnAndRow[0];
|
||||
mSelY1 = mSelY2 = columnAndRow[1];
|
||||
|
||||
TerminalBuffer screen = terminalView.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 < terminalView.mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
|
||||
mSelX2++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setActionModeCallBacks() {
|
||||
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
int show = MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard != null && clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_MORE, 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) {
|
||||
if (!isActive()) {
|
||||
// Fix issue where the dialog is pressed while being dismissed.
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case ACTION_COPY:
|
||||
String selectedText = getSelectedText();
|
||||
terminalView.mTermSession.onCopyTextToClipboard(selectedText);
|
||||
terminalView.stopTextSelectionMode();
|
||||
break;
|
||||
case ACTION_PASTE:
|
||||
terminalView.stopTextSelectionMode();
|
||||
terminalView.mTermSession.onPasteTextFromClipboard();
|
||||
break;
|
||||
case ACTION_MORE:
|
||||
// We first store the selected text in case TerminalViewClient needs the
|
||||
// selected text before MORE button was pressed since we are going to
|
||||
// stop selection mode
|
||||
mStoredSelectedText = getSelectedText();
|
||||
// The text selection needs to be stopped before showing context menu,
|
||||
// otherwise handles will show above popup
|
||||
terminalView.stopTextSelectionMode();
|
||||
terminalView.showContextMenu();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
mActionMode = terminalView.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 * terminalView.mRenderer.getFontWidth());
|
||||
int x2 = Math.round(mSelX2 * terminalView.mRenderer.getFontWidth());
|
||||
int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
|
||||
int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
|
||||
|
||||
if (x1 > x2) {
|
||||
int tmp = x1;
|
||||
x1 = x2;
|
||||
x2 = tmp;
|
||||
}
|
||||
|
||||
int terminalBottom = terminalView.getBottom();
|
||||
int top = y1 + mHandleHeight;
|
||||
int bottom = y2 + mHandleHeight;
|
||||
if (top > terminalBottom) top = terminalBottom;
|
||||
if (bottom > terminalBottom) bottom = terminalBottom;
|
||||
|
||||
outRect.set(x1, top, x2, bottom);
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePosition(TextSelectionHandleView handle, int x, int y) {
|
||||
TerminalBuffer screen = terminalView.mEmulator.getScreen();
|
||||
final int scrollRows = screen.getActiveRows() - terminalView.mEmulator.mRows;
|
||||
if (handle == mStartHandle) {
|
||||
mSelX1 = terminalView.getCursorX(x);
|
||||
mSelY1 = terminalView.getCursorY(y);
|
||||
if (mSelX1 < 0) {
|
||||
mSelX1 = 0;
|
||||
}
|
||||
|
||||
if (mSelY1 < -scrollRows) {
|
||||
mSelY1 = -scrollRows;
|
||||
|
||||
} else if (mSelY1 > terminalView.mEmulator.mRows - 1) {
|
||||
mSelY1 = terminalView.mEmulator.mRows - 1;
|
||||
|
||||
}
|
||||
|
||||
if (mSelY1 > mSelY2) {
|
||||
mSelY1 = mSelY2;
|
||||
}
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
|
||||
mSelX1 = mSelX2;
|
||||
}
|
||||
|
||||
if (!terminalView.mEmulator.isAlternateBufferActive()) {
|
||||
int topRow = terminalView.getTopRow();
|
||||
|
||||
if (mSelY1 <= topRow) {
|
||||
topRow--;
|
||||
if (topRow < -scrollRows) {
|
||||
topRow = -scrollRows;
|
||||
}
|
||||
} else if (mSelY1 >= topRow + terminalView.mEmulator.mRows) {
|
||||
topRow++;
|
||||
if (topRow > 0) {
|
||||
topRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
terminalView.setTopRow(topRow);
|
||||
}
|
||||
|
||||
mSelX1 = getValidCurX(screen, mSelY1, mSelX1);
|
||||
|
||||
} else {
|
||||
mSelX2 = terminalView.getCursorX(x);
|
||||
mSelY2 = terminalView.getCursorY(y);
|
||||
if (mSelX2 < 0) {
|
||||
mSelX2 = 0;
|
||||
}
|
||||
|
||||
if (mSelY2 < -scrollRows) {
|
||||
mSelY2 = -scrollRows;
|
||||
} else if (mSelY2 > terminalView.mEmulator.mRows - 1) {
|
||||
mSelY2 = terminalView.mEmulator.mRows - 1;
|
||||
}
|
||||
|
||||
if (mSelY1 > mSelY2) {
|
||||
mSelY2 = mSelY1;
|
||||
}
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
|
||||
mSelX2 = mSelX1;
|
||||
}
|
||||
|
||||
if (!terminalView.mEmulator.isAlternateBufferActive()) {
|
||||
int topRow = terminalView.getTopRow();
|
||||
|
||||
if (mSelY2 <= topRow) {
|
||||
topRow--;
|
||||
if (topRow < -scrollRows) {
|
||||
topRow = -scrollRows;
|
||||
}
|
||||
} else if (mSelY2 >= topRow + terminalView.mEmulator.mRows) {
|
||||
topRow++;
|
||||
if (topRow > 0) {
|
||||
topRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
terminalView.setTopRow(topRow);
|
||||
}
|
||||
|
||||
mSelX2 = getValidCurX(screen, mSelY2, mSelX2);
|
||||
}
|
||||
|
||||
terminalView.invalidate();
|
||||
}
|
||||
|
||||
private int getValidCurX(TerminalBuffer screen, int cy, int cx) {
|
||||
String line = screen.getSelectedText(0, cy, cx, cy);
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
int col = 0;
|
||||
for (int i = 0, len = line.length(); i < len; i++) {
|
||||
char ch1 = line.charAt(i);
|
||||
if (ch1 == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
int wc;
|
||||
if (Character.isHighSurrogate(ch1) && i + 1 < len) {
|
||||
char ch2 = line.charAt(++i);
|
||||
wc = WcWidth.width(Character.toCodePoint(ch1, ch2));
|
||||
} else {
|
||||
wc = WcWidth.width(ch1);
|
||||
}
|
||||
|
||||
final int cend = col + wc;
|
||||
if (cx > col && cx < cend) {
|
||||
return cend;
|
||||
}
|
||||
if (cend == col) {
|
||||
return col;
|
||||
}
|
||||
col = cend;
|
||||
}
|
||||
}
|
||||
return cx;
|
||||
}
|
||||
|
||||
public void decrementYTextSelectionCursors(int decrement) {
|
||||
mSelY1 -= decrement;
|
||||
mSelY2 -= decrement;
|
||||
}
|
||||
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onTouchModeChanged(boolean isInTouchMode) {
|
||||
if (!isInTouchMode) {
|
||||
terminalView.stopTextSelectionMode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetached() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return mIsSelectingText;
|
||||
}
|
||||
|
||||
public void getSelectors(int[] sel) {
|
||||
if (sel == null || sel.length != 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
sel[0] = mSelY1;
|
||||
sel[1] = mSelY2;
|
||||
sel[2] = mSelX1;
|
||||
sel[3] = mSelX2;
|
||||
}
|
||||
|
||||
/** Get the currently selected text. */
|
||||
public String getSelectedText() {
|
||||
return terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2);
|
||||
}
|
||||
|
||||
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
@Nullable
|
||||
public String getStoredSelectedText() {
|
||||
return mStoredSelectedText;
|
||||
}
|
||||
|
||||
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
public void unsetStoredSelectedText() {
|
||||
mStoredSelectedText = null;
|
||||
}
|
||||
|
||||
public ActionMode getActionMode() {
|
||||
return mActionMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this controller is currently used to move the start selection.
|
||||
*/
|
||||
public boolean isSelectionStartDragged() {
|
||||
return mStartHandle.isDragging();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this controller is currently used to move the end selection.
|
||||
*/
|
||||
public boolean isSelectionEndDragged() {
|
||||
return mEndHandle.isDragging();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
package com.termux.view.textselection;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import com.termux.view.R;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class TextSelectionHandleView extends View {
|
||||
private final TerminalView terminalView;
|
||||
private PopupWindow mHandle;
|
||||
private final CursorController mCursorController;
|
||||
|
||||
private final Drawable mHandleLeftDrawable;
|
||||
private final Drawable mHandleRightDrawable;
|
||||
private Drawable mHandleDrawable;
|
||||
|
||||
private boolean mIsDragging;
|
||||
|
||||
final int[] mTempCoords = new int[2];
|
||||
Rect mTempRect;
|
||||
|
||||
private int mPointX;
|
||||
private int mPointY;
|
||||
private float mTouchToWindowOffsetX;
|
||||
private float mTouchToWindowOffsetY;
|
||||
private float mHotspotX;
|
||||
private float mHotspotY;
|
||||
private float mTouchOffsetY;
|
||||
private int mLastParentX;
|
||||
private int mLastParentY;
|
||||
|
||||
private int mHandleHeight;
|
||||
private int mHandleWidth;
|
||||
|
||||
private final int mInitialOrientation;
|
||||
private int mOrientation;
|
||||
|
||||
public static final int LEFT = 0;
|
||||
public static final int RIGHT = 2;
|
||||
|
||||
private long mLastTime;
|
||||
|
||||
public TextSelectionHandleView(TerminalView terminalView, CursorController cursorController, int initialOrientation) {
|
||||
super(terminalView.getContext());
|
||||
this.terminalView = terminalView;
|
||||
mCursorController = cursorController;
|
||||
mInitialOrientation = initialOrientation;
|
||||
|
||||
mHandleLeftDrawable = getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
||||
mHandleRightDrawable = getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
||||
|
||||
setOrientation(mInitialOrientation);
|
||||
}
|
||||
|
||||
private void initHandle() {
|
||||
mHandle = new PopupWindow(terminalView.getContext(), null,
|
||||
android.R.attr.textSelectHandleWindowStyle);
|
||||
mHandle.setSplitTouchEnabled(true);
|
||||
mHandle.setClippingEnabled(false);
|
||||
mHandle.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
|
||||
mHandle.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mHandle.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mHandle.setBackgroundDrawable(null);
|
||||
mHandle.setAnimationStyle(0);
|
||||
mHandle.setEnterTransition(null);
|
||||
mHandle.setExitTransition(null);
|
||||
mHandle.setContentView(this);
|
||||
}
|
||||
|
||||
public void setOrientation(int orientation) {
|
||||
mOrientation = orientation;
|
||||
int handleWidth = 0;
|
||||
switch (orientation) {
|
||||
case LEFT: {
|
||||
mHandleDrawable = mHandleLeftDrawable;
|
||||
handleWidth = mHandleDrawable.getIntrinsicWidth();
|
||||
mHotspotX = (handleWidth * 3) / (float) 4;
|
||||
break;
|
||||
}
|
||||
|
||||
case RIGHT: {
|
||||
mHandleDrawable = mHandleRightDrawable;
|
||||
handleWidth = mHandleDrawable.getIntrinsicWidth();
|
||||
mHotspotX = handleWidth / (float) 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mHandleHeight = mHandleDrawable.getIntrinsicHeight();
|
||||
|
||||
mHandleWidth = handleWidth;
|
||||
mTouchOffsetY = -mHandleHeight * 0.3f;
|
||||
mHotspotY = 0;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!isPositionVisible()) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// We remove handle from its parent first otherwise the following exception may be thrown
|
||||
// java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
|
||||
removeFromParent();
|
||||
|
||||
initHandle(); // init the handle
|
||||
invalidate(); // invalidate to make sure onDraw is called
|
||||
|
||||
final int[] coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
coords[0] += mPointX;
|
||||
coords[1] += mPointY;
|
||||
|
||||
if (mHandle != null)
|
||||
mHandle.showAtLocation(terminalView, 0, coords[0], coords[1]);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
mIsDragging = false;
|
||||
|
||||
if (mHandle != null) {
|
||||
mHandle.dismiss();
|
||||
|
||||
// We remove handle from its parent, otherwise it may still be shown in some cases even after the dismiss call
|
||||
removeFromParent();
|
||||
mHandle = null; // garbage collect the handle
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void removeFromParent() {
|
||||
if (!isParentNull()) {
|
||||
((ViewGroup)this.getParent()).removeView(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void positionAtCursor(final int cx, final int cy, boolean forceOrientationCheck) {
|
||||
int x = terminalView.getPointX(cx);
|
||||
int y = terminalView.getPointY(cy + 1);
|
||||
moveTo(x, y, forceOrientationCheck);
|
||||
}
|
||||
|
||||
private void moveTo(int x, int y, boolean forceOrientationCheck) {
|
||||
float oldHotspotX = mHotspotX;
|
||||
checkChangedOrientation(x, forceOrientationCheck);
|
||||
mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX));
|
||||
mPointY = y;
|
||||
|
||||
if (isPositionVisible()) {
|
||||
int[] coords = null;
|
||||
|
||||
if (isShowing()) {
|
||||
coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
int x1 = coords[0] + mPointX;
|
||||
int y1 = coords[1] + mPointY;
|
||||
if (mHandle != null)
|
||||
mHandle.update(x1, y1, getWidth(), getHeight());
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
|
||||
if (mIsDragging) {
|
||||
if (coords == null) {
|
||||
coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
}
|
||||
if (coords[0] != mLastParentX || coords[1] != mLastParentY) {
|
||||
mTouchToWindowOffsetX += coords[0] - mLastParentX;
|
||||
mTouchToWindowOffsetY += coords[1] - mLastParentY;
|
||||
mLastParentX = coords[0];
|
||||
mLastParentY = coords[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
public void changeOrientation(int orientation) {
|
||||
if (mOrientation != orientation) {
|
||||
setOrientation(orientation);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkChangedOrientation(int posX, boolean force) {
|
||||
if (!mIsDragging && !force) {
|
||||
return;
|
||||
}
|
||||
long millis = SystemClock.currentThreadTimeMillis();
|
||||
if (millis - mLastTime < 50 && !force) {
|
||||
return;
|
||||
}
|
||||
mLastTime = millis;
|
||||
|
||||
final TerminalView hostView = terminalView;
|
||||
final int left = hostView.getLeft();
|
||||
final int right = hostView.getWidth();
|
||||
final int top = hostView.getTop();
|
||||
final int bottom = hostView.getHeight();
|
||||
|
||||
if (mTempRect == null) {
|
||||
mTempRect = new Rect();
|
||||
}
|
||||
final Rect clip = mTempRect;
|
||||
clip.left = left + terminalView.getPaddingLeft();
|
||||
clip.top = top + terminalView.getPaddingTop();
|
||||
clip.right = right - terminalView.getPaddingRight();
|
||||
clip.bottom = bottom - terminalView.getPaddingBottom();
|
||||
|
||||
final ViewParent parent = hostView.getParent();
|
||||
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (posX - mHandleWidth < clip.left) {
|
||||
changeOrientation(RIGHT);
|
||||
} else if (posX + mHandleWidth > clip.right) {
|
||||
changeOrientation(LEFT);
|
||||
} else {
|
||||
changeOrientation(mInitialOrientation);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPositionVisible() {
|
||||
// Always show a dragging handle.
|
||||
if (mIsDragging) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final TerminalView hostView = terminalView;
|
||||
final int left = 0;
|
||||
final int right = hostView.getWidth();
|
||||
final int top = 0;
|
||||
final int bottom = hostView.getHeight();
|
||||
|
||||
if (mTempRect == null) {
|
||||
mTempRect = new Rect();
|
||||
}
|
||||
final Rect clip = mTempRect;
|
||||
clip.left = left + terminalView.getPaddingLeft();
|
||||
clip.top = top + terminalView.getPaddingTop();
|
||||
clip.right = right - terminalView.getPaddingRight();
|
||||
clip.bottom = bottom - terminalView.getPaddingBottom();
|
||||
|
||||
final ViewParent parent = hostView.getParent();
|
||||
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int[] coords = mTempCoords;
|
||||
hostView.getLocationInWindow(coords);
|
||||
final int posX = coords[0] + mPointX + (int) mHotspotX;
|
||||
final int posY = coords[1] + mPointY + (int) mHotspotY;
|
||||
|
||||
return posX >= clip.left && posX <= clip.right &&
|
||||
posY >= clip.top && posY <= clip.bottom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c) {
|
||||
final int width = mHandleDrawable.getIntrinsicWidth();
|
||||
int height = mHandleDrawable.getIntrinsicHeight();
|
||||
mHandleDrawable.setBounds(0, 0, width, height);
|
||||
mHandleDrawable.draw(c);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
terminalView.updateFloatingToolbarVisibility(event);
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
final float rawX = event.getRawX();
|
||||
final float rawY = event.getRawY();
|
||||
mTouchToWindowOffsetX = rawX - mPointX;
|
||||
mTouchToWindowOffsetY = rawY - mPointY;
|
||||
final int[] coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
mLastParentX = coords[0];
|
||||
mLastParentY = coords[1];
|
||||
mIsDragging = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
final float rawX = event.getRawX();
|
||||
final float rawY = event.getRawY();
|
||||
|
||||
final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
|
||||
final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY;
|
||||
|
||||
mCursorController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mIsDragging = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
setMeasuredDimension(mHandleDrawable.getIntrinsicWidth(),
|
||||
mHandleDrawable.getIntrinsicHeight());
|
||||
}
|
||||
|
||||
public int getHandleHeight() {
|
||||
return mHandleHeight;
|
||||
}
|
||||
|
||||
public int getHandleWidth() {
|
||||
return mHandleWidth;
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
if (mHandle != null)
|
||||
return mHandle.isShowing();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isParentNull() {
|
||||
return this.getParent() == null;
|
||||
}
|
||||
|
||||
public boolean isDragging() {
|
||||
return mIsDragging;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="132"
|
||||
android:viewportHeight="66">
|
||||
<path
|
||||
android:pathData="M52.3,1.6c-5.7,2.1 -12.9,8.6 -16,14.8 -2.2,4.1 -2.8,6.9 -3.1,14.3 -0.6,12.6 1.3,17.8 9.3,25.8 8,8 13.2,9.9 25.8,9.3 11.1,-0.5 17.3,-3.2 23.5,-10.3 6.5,-7.4 7.2,-10.8 7.2,-34.7l0,-20.8 -21.2,0.1c-16.1,-0 -22.3,0.4 -25.5,1.5z"
|
||||
android:fillColor="#2196F3"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="132"
|
||||
android:viewportHeight="66">
|
||||
<path
|
||||
android:pathData="M33,20.8c0,23.9 0.7,27.3 7.2,34.7 6.2,7.1 12.4,9.8 23.5,10.3 12.6,0.6 17.8,-1.3 25.8,-9.3 8,-8 9.9,-13.2 9.3,-25.8 -0.5,-11.1 -3.2,-17.3 -10.3,-23.5 -7.4,-6.5 -10.8,-7.2 -34.7,-7.2l-20.8,-0 0,20.8z"
|
||||
android:fillColor="#2196F3"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue