1
0
mirror of https://github.com/termux/termux-app synced 2024-06-17 22:57:08 +00:00
This commit is contained in:
mao 2024-05-02 23:09:28 +03:00 committed by GitHub
commit 9ab0270bff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 429 additions and 18 deletions

View File

@ -2,12 +2,9 @@ package com.termux.app.terminal;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.media.AudioManager;
import android.os.Environment;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
@ -15,7 +12,6 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import com.termux.R;
import com.termux.app.TermuxActivity;
@ -216,6 +212,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
return mActivity.getProperties().isEnforcingCharBasedInput();
}
@Override
public boolean isInputComposingDisabled() {
return mActivity.getProperties().isInputComposingDisabled();
}
@Override
public boolean shouldUseCtrlSpaceWorkaround() {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();

View File

@ -0,0 +1,71 @@
package com.termux.view;
import android.content.Context;
import android.text.Editable;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.TextWatcher;
import android.view.inputmethod.InputMethodManager;
import com.termux.view.inputmethod.TerminalInputConnection;
class ChangeWatcher implements TextWatcher, SpanWatcher {
final TerminalView mTerminalView;
ChangeWatcher(TerminalView terminalView) {
this.mTerminalView = terminalView;
}
private void sendUpdateSelection() {
final InputMethodManager imm = (InputMethodManager) mTerminalView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
final Editable text = mTerminalView.getImeBuffer();
if (imm != null && text != null) {
final int selectionStart = Selection.getSelectionStart(text);
final int selectionEnd = Selection.getSelectionEnd(text);
final int candStart = TerminalInputConnection.getComposingSpanStart(text);
final int candEnd = TerminalInputConnection.getComposingSpanEnd(text);
imm.updateSelection(mTerminalView, selectionStart, selectionEnd, candStart, candEnd);
}
}
private void spanChange(Object what) {
if (what == Selection.SELECTION_START || what == Selection.SELECTION_END) {
sendUpdateSelection();
}
}
@Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {
spanChange(what);
}
@Override
public void onSpanRemoved(Spannable text, Object what, int start, int end) {
spanChange(what);
}
@Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
spanChange(what);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
mTerminalView.invalidate();
}
}

View File

@ -1,6 +1,7 @@
package com.termux.view;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
@ -55,7 +56,8 @@ public final class TerminalRenderer {
/** 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) {
int selectionY1, int selectionY2, int selectionX1, int selectionX2,
CharSequence imeBuffer) {
final boolean reverseVideo = mEmulator.isReverseVideo();
final int endRow = topRow + mEmulator.mRows;
final int columns = mEmulator.mColumns;
@ -153,6 +155,12 @@ public final class TerminalRenderer {
}
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
if (row == cursorRow) {
//ime fore color
int imeFore = Color.DKGRAY;
drawImeBuffer(canvas, heightOffset, cursorCol, mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR], imeFore, imeBuffer, columns);
}
}
}
@ -239,6 +247,35 @@ public final class TerminalRenderer {
if (savedMatrix) canvas.restore();
}
private void drawImeBuffer(Canvas canvas, float y, int cursorX, int cursorColor, int imeFore, CharSequence imeBuffer, int columns) {
// draw ime buffer
final int bufLen = imeBuffer.length();
if (bufLen > 0) {
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
final float left = cursorX * mFontWidth;
final int maxWidth = (int) (columns * mFontWidth - left);
if (maxWidth > 0) {
final float[] widths = bufLen > asciiMeasures.length ? new float[bufLen] : asciiMeasures;
final int count = mTextPaint.getTextWidths(imeBuffer, 0, bufLen, widths);
int offset = count - 1;
int width = 0;
for (; offset >= 0 && width < maxWidth; offset--) {
width += widths[offset];
}
offset++;
mTextPaint.setColor(cursorColor);
canvas.drawRect(left, y - cursorHeight, left + width,
y, mTextPaint);
mTextPaint.setColor(imeFore);
canvas.drawText(imeBuffer, offset, bufLen, left, y - mFontLineSpacingAndAscent, mTextPaint);
}
}
}
public float getFontWidth() {
return mFontWidth;
}

View File

@ -14,6 +14,8 @@ import android.os.Looper;
import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ActionMode;
@ -39,6 +41,7 @@ import androidx.annotation.RequiresApi;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.view.inputmethod.TerminalInputConnection;
import com.termux.view.textselection.TextSelectionCursorController;
/** View displaying and interacting with a {@link TerminalSession}. */
@ -95,6 +98,7 @@ public final class TerminalView extends View {
private static final String LOG_TAG = "TerminalView";
private final SpannableStringBuilder imeBuffer = new SpannableStringBuilder("");
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
super(context, attributes);
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
@ -221,6 +225,11 @@ public final class TerminalView extends View {
mScroller = new Scroller(context);
AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mAccessibilityEnabled = am.isEnabled();
imeBuffer.setSpan(new ChangeWatcher(this),
0, imeBuffer.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE | (100 << Spanned.SPAN_PRIORITY_SHIFT));
}
@ -265,8 +274,25 @@ public final class TerminalView extends View {
return true;
}
public Editable getImeBuffer() {
return imeBuffer;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if(!mClient.isInputComposingDisabled()){
outAttrs.imeOptions =
EditorInfo.IME_FLAG_NO_EXTRACT_UI |
EditorInfo.IME_FLAG_NO_FULLSCREEN |
EditorInfo.IME_FLAG_NO_ENTER_ACTION;
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
outAttrs.hintText = "";
outAttrs.initialSelStart = -1;
outAttrs.initialSelEnd = -1;
outAttrs.initialCapsMode = 0;
return new TerminalInputConnection(this);
}
// Ensure that inputType is only set if TerminalView is selected view with the keyboard and
// an alternate view is not selected, like an EditText. This is necessary if an activity is
// initially started with the alternate view or if activity is returned to from another app
@ -975,7 +1001,7 @@ public final class TerminalView extends View {
mTextSelectionCursorController.getSelectors(sel);
}
mRenderer.render(mEmulator, canvas, mTopRow, sel[0], sel[1], sel[2], sel[3]);
mRenderer.render(mEmulator, canvas, mTopRow, sel[0], sel[1], sel[2], sel[3], imeBuffer);
// render the text selection handles
renderTextSelection();

View File

@ -36,7 +36,7 @@ public interface TerminalViewClient {
boolean isTerminalViewSelected();
boolean isInputComposingDisabled();
void copyModeChanged(boolean copyMode);

View File

@ -0,0 +1,260 @@
package com.termux.view.inputmethod;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.Selection;
import android.util.Log;
import android.view.KeyEvent;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import com.termux.terminal.TerminalEmulator;
import com.termux.view.TerminalView;
/**
*
*/
public class TerminalInputConnection extends BaseInputConnection {
private static final boolean DEBUG = false;
private static final String TAG = "TerminalInputConnection";
private final Editable editable;
private final TerminalView mTargetView;
public TerminalInputConnection(TerminalView targetView) {
super(targetView, true);
mTargetView = targetView;
editable = targetView.getImeBuffer();
editable.clear();
Selection.setSelection(editable, 0);
}
@Override
public Editable getEditable() {
return editable;
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (DEBUG) Log.d(TAG, "commitText: " + text.length() + " " + text.getClass() + " ");
//Some input methods convert the enter key to '\n', split it
boolean sendEnter = false;
final int last;
if (text != null && (last = text.length() - 1) >= 0 && text.charAt(last) == '\n') {
text = text.subSequence(0, last);
sendEnter = true;
}
super.commitText(text, newCursorPosition);
final Editable editable = getEditable();
sendTextToTerminal(editable);
editable.clear();
if (sendEnter) {
sendEnterKey();
try {
// Simulate insert '\n', then clear()
InputMethodManager imm = (InputMethodManager) mTargetView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.updateSelection(mTargetView, 1, 1, -1, -1);
imm.updateSelection(mTargetView, 0, 0, -1, -1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
private void sendEnterKey() {
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
if (DEBUG) Log.d(TAG, "getExtractedText: " + flags);
final ExtractedText extract = new ExtractedText();
final Editable editable = getEditable();
extract.startOffset = 0;
int a = Selection.getSelectionStart(editable);
int b = Selection.getSelectionEnd(editable);
if (a > b) {
int tmp = a;
a = b;
b = tmp;
}
extract.selectionStart = a;
extract.selectionEnd = b;
extract.text = editable;
return extract;
}
@Override
public boolean beginBatchEdit() {
if (DEBUG) Log.d(TAG, "beginBatchEdit: ");
return super.beginBatchEdit();
}
@Override
public boolean endBatchEdit() {
if (DEBUG) Log.d(TAG, "endBatchEdit: ");
return super.endBatchEdit();
}
@Override
public boolean clearMetaKeyStates(int states) {
if (DEBUG) Log.d(TAG, "clearMetaKeyStates: ");
return super.clearMetaKeyStates(states);
}
@Override
public CharSequence getTextAfterCursor(int length, int flags) {
if (DEBUG) Log.d(TAG, "getTextAfterCursor: " + length + " " + flags);
return super.getTextAfterCursor(length, flags);
}
@Override
public CharSequence getTextBeforeCursor(int length, int flags) {
if (DEBUG) Log.d(TAG, "getTextBeforeCursor: " + length + " " + flags);
return super.getTextBeforeCursor(length, flags);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (DEBUG) Log.d(TAG, "setComposingText: " + text + " " + newCursorPosition);
return super.setComposingText(text, newCursorPosition);
}
@Override
public boolean setComposingRegion(int start, int end) {
if (DEBUG) Log.d(TAG, "setComposingRegion: " + start + " " + end);
return super.setComposingRegion(start, end);
}
@Override
public boolean setSelection(int start, int end) {
if (DEBUG) Log.d(TAG, "setSelection: ");
return super.setSelection(start, end);
}
@Override
public boolean deleteSurroundingText(int leftLength, int rightLength) {
if (DEBUG) {
Log.d(TAG, "deleteSurroundingText: " + leftLength + " " + rightLength);
}
return super.deleteSurroundingText(leftLength, rightLength);
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
if (DEBUG) Log.d(TAG, "sendKeyEvent: " + event.getDisplayLabel());
return super.sendKeyEvent(event);
}
@Override
public boolean finishComposingText() {
if (DEBUG) {
Log.d(TAG, "finishComposingText: " + getEditable() + " " + getComposingSpanStart(getEditable()) + " " + getComposingSpanEnd(getEditable()));
}
super.finishComposingText();
final Editable editable = getEditable();
sendTextToTerminal(editable);
editable.clear();
return true;
}
@Override
public int getCursorCapsMode(int reqModes) {
if (DEBUG) Log.d(TAG, "getCursorCapsMode: " + reqModes);
return super.getCursorCapsMode(reqModes);
}
@Override
public boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags, Bundle opts) {
if (DEBUG) Log.d(TAG, "commitContent: ");
return super.commitContent(inputContentInfo, flags, opts);
}
@Override
public boolean commitCompletion(CompletionInfo text) {
if (DEBUG) Log.d(TAG, "commitCompletion: " + text.getText());
return super.commitCompletion(text);
}
private void sendTextToTerminal(CharSequence text) {
mTargetView.stopTextSelectionMode();
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;
}
// Check onKeyDown() for details.
if (mTargetView.mClient.readShiftKey())
codePoint = Character.toUpperCase(codePoint);
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;
}
}
mTargetView.inputCodePoint(TerminalView.KEY_EVENT_SOURCE_SOFT_KEYBOARD, codePoint, ctrlHeld, false);
}
}
}

View File

@ -305,6 +305,8 @@ public class TextSelectionCursorController implements CursorController {
}
private int getValidCurX(TerminalBuffer screen, int cy, int cx) {
//Cursor drawing incompatible.
/*
String line = screen.getSelectedText(0, cy, cx, cy);
if (!TextUtils.isEmpty(line)) {
int col = 0;
@ -332,6 +334,7 @@ public class TextSelectionCursorController implements CursorController {
col = cend;
}
}
*/
return cx;
}

View File

@ -47,6 +47,7 @@ public class TextSelectionHandleView extends View {
private final int mInitialOrientation;
private int mOrientation;
private float mAdjHotspotOffsetX = 0;
public static final int LEFT = 0;
public static final int RIGHT = 2;
@ -149,7 +150,7 @@ public class TextSelectionHandleView extends View {
public void removeFromParent() {
if (!isParentNull()) {
((ViewGroup)this.getParent()).removeView(this);
((ViewGroup) this.getParent()).removeView(this);
}
}
@ -160,10 +161,9 @@ public class TextSelectionHandleView extends View {
}
private void moveTo(int x, int y, boolean forceOrientationCheck) {
float oldHotspotX = mHotspotX;
checkChangedOrientation(x, forceOrientationCheck);
mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX));
mPointX = (int) (x - mHotspotX);
mPointY = y;
checkChangedOrientation(forceOrientationCheck);
if (isPositionVisible()) {
int[] coords = null;
@ -198,11 +198,14 @@ public class TextSelectionHandleView extends View {
public void changeOrientation(int orientation) {
if (mOrientation != orientation) {
final float hotspotX = mHotspotX;
setOrientation(orientation);
mAdjHotspotOffsetX = (mHotspotX - hotspotX);
mTouchToWindowOffsetX += mAdjHotspotOffsetX;
}
}
private void checkChangedOrientation(int posX, boolean force) {
private void checkChangedOrientation(boolean force) {
if (!mIsDragging && !force) {
return;
}
@ -231,6 +234,7 @@ public class TextSelectionHandleView extends View {
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
return;
}
final int posX = (int) (mPointX + mAdjHotspotOffsetX);
if (posX - mHandleWidth < clip.left) {
changeOrientation(RIGHT);
@ -315,6 +319,7 @@ public class TextSelectionHandleView extends View {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mAdjHotspotOffsetX = 0;
mIsDragging = false;
}
return true;

View File

@ -3,18 +3,13 @@ package com.termux.shared.termux.settings.properties;
import com.google.common.collect.ImmutableBiMap;
import com.termux.shared.termux.shell.am.TermuxAmSocketServer;
import com.termux.shared.theme.NightMode;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.settings.properties.SharedProperties;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalEmulator;
import com.termux.view.TerminalView;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/*
@ -121,6 +116,8 @@ public final class TermuxPropertyConstants {
public static final String KEY_ENFORCE_CHAR_BASED_INPUT = "enforce-char-based-input"; // Default: "enforce-char-based-input"
public static final String KEY_DISABLE_INPUT_COMPOSING = "disable-input-composing";
/** Defines the key for whether text for the extra keys buttons should be all capitalized automatically */
public static final String KEY_EXTRA_KEYS_TEXT_ALL_CAPS = "extra-keys-text-all-caps"; // Default: "extra-keys-text-all-caps"
@ -396,6 +393,7 @@ public final class TermuxPropertyConstants {
KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS,
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
KEY_ENFORCE_CHAR_BASED_INPUT,
KEY_DISABLE_INPUT_COMPOSING,
KEY_EXTRA_KEYS_TEXT_ALL_CAPS,
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
KEY_RUN_TERMUX_AM_SOCKET_SERVER,
@ -459,7 +457,8 @@ public final class TermuxPropertyConstants {
*/
public static final Set<String> TERMUX_DEFAULT_TRUE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
KEY_EXTRA_KEYS_TEXT_ALL_CAPS,
KEY_RUN_TERMUX_AM_SOCKET_SERVER
KEY_RUN_TERMUX_AM_SOCKET_SERVER,
KEY_DISABLE_INPUT_COMPOSING
));
/** Defines the set for keys loaded by termux that have default inverted boolean behaviour with false as default.

View File

@ -598,6 +598,10 @@ public abstract class TermuxSharedProperties {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
}
public boolean isInputComposingDisabled(){
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_INPUT_COMPOSING, true);
}
public boolean shouldExtraKeysTextBeAllCaps() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_TEXT_ALL_CAPS, true);
}

View File

@ -29,6 +29,11 @@ public class TermuxTerminalViewClientBase implements TerminalViewClient {
return false;
}
@Override
public boolean isInputComposingDisabled() {
return true;
}
public boolean shouldUseCtrlSpaceWorkaround() {
return false;
}