From 76de18e1b6e6abd9504c19be8b2f5e8c921572eb Mon Sep 17 00:00:00 2001 From: fifth_light Date: Thu, 14 Aug 2025 21:21:56 +0800 Subject: [PATCH] feat[TouchController]: add text input intergation --- .../net/kdt/pojavlaunch/MainActivity.java | 13 +- .../utils/TouchControllerInputView.java | 693 ++++++++++++++++++ .../utils/TouchControllerUtils.java | 8 +- .../src/main/res/layout/activity_basemain.xml | 5 + 4 files changed, 714 insertions(+), 5 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerInputView.java diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index 757c7579b7..7a9aff0e7c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -63,6 +63,7 @@ import net.kdt.pojavlaunch.services.GameService; import net.kdt.pojavlaunch.utils.JREUtils; import net.kdt.pojavlaunch.utils.MCOptionUtils; +import net.kdt.pojavlaunch.utils.TouchControllerInputView; import net.kdt.pojavlaunch.utils.TouchControllerUtils; import net.kdt.pojavlaunch.value.MinecraftAccount; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; @@ -82,6 +83,7 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe volatile public static boolean isInputStackCall; public static TouchCharInput touchCharInput; + private TouchControllerInputView touchControllerInputView; private MinecraftGLSurface minecraftGLView; private static Touchpad touchpad; private LoggerView loggerView; @@ -110,9 +112,6 @@ public void onCreate(Bundle savedInstanceState) { String gameDirPath = Tools.getGameDirPath(minecraftProfile).getAbsolutePath(); MCOptionUtils.load(gameDirPath); - if (Tools.hasTouchController(new File(gameDirPath)) || LauncherPreferences.PREF_FORCE_ENABLE_TOUCHCONTROLLER) { - TouchControllerUtils.initialize(this); - } Intent gameServiceIntent = new Intent(this, GameService.class); // Start the service a bit early @@ -121,6 +120,10 @@ public void onCreate(Bundle savedInstanceState) { CallbackBridge.addGrabListener(touchpad); CallbackBridge.addGrabListener(minecraftGLView); + if (Tools.hasTouchController(new File(gameDirPath)) || LauncherPreferences.PREF_FORCE_ENABLE_TOUCHCONTROLLER) { + TouchControllerUtils.initialize(this, touchControllerInputView); + } + mGyroControl = new GyroControl(this); // Enabling this on TextureView results in a broken white result @@ -174,6 +177,8 @@ protected void initLayout(int resId) { GLOBAL_CLIPBOARD = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); touchCharInput.setCharacterSender(new LwjglCharSender()); + touchControllerInputView.setSize(minecraftGLView.getWidth(), minecraftGLView.getHeight()); + if(minecraftProfile.pojavRendererName != null) { Log.i("RdrDebug","__P_renderer="+minecraftProfile.pojavRendererName); Tools.LOCAL_RENDERER = minecraftProfile.pojavRendererName; @@ -271,6 +276,7 @@ private void bindValues(){ loggerView = findViewById(R.id.mainLoggerView); mControlLayout = findViewById(R.id.main_control_layout); touchCharInput = findViewById(R.id.mainTouchCharInput); + touchControllerInputView = findViewById(R.id.touch_controller_input); mDrawerPullButton = findViewById(R.id.drawer_button); mHotbarView = findViewById(R.id.hotbar_view); } @@ -329,6 +335,7 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) { minecraftGLView.refreshSize(); Tools.updateWindowSize(this); mControlLayout.refreshControlButtonPositions(); + touchControllerInputView.setSize(minecraftGLView.getWidth(), minecraftGLView.getHeight()); }); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerInputView.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerInputView.java new file mode 100644 index 0000000000..707a711a3e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerInputView.java @@ -0,0 +1,693 @@ +package net.kdt.pojavlaunch.utils; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.text.InputType; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorBoundsInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.InputMethodManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.view.inputmethod.EditorInfoCompat; + +import top.fifthlight.touchcontroller.proxy.client.LauncherProxyClient; +import top.fifthlight.touchcontroller.proxy.message.FloatRect; +import top.fifthlight.touchcontroller.proxy.message.input.TextInputState; +import top.fifthlight.touchcontroller.proxy.message.input.TextInputStateKt; +import top.fifthlight.touchcontroller.proxy.message.input.TextRange; + +import static top.fifthlight.touchcontroller.proxy.message.input.TextInputStateKt.getCompositionText; +import static top.fifthlight.touchcontroller.proxy.message.input.TextInputStateKt.getSelectionText; + +import net.kdt.pojavlaunch.EfficientAndroidLWJGLKeycode; +import net.kdt.pojavlaunch.customcontrols.keyboard.CharacterSenderStrategy; + +public class TouchControllerInputView extends View { + public boolean disableFullScreenInput = false; + private LauncherProxyClient client; + private int width = -1; + private int height = -1; + @Nullable + public CharacterSenderStrategy characterSenderStrategy; + private final InputMethodManager inputMethodManager; + + private TextInputState inputState; + private FloatRect cursorRect; + private FloatRect inputAreaRect; + private InputConnectionImpl inputConnection; + private final LauncherProxyClient.InputHandler inputHandler; + + public TouchControllerInputView(Context context) { + this(context, null); + } + + public TouchControllerInputView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TouchControllerInputView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + inputMethodManager = ContextCompat.getSystemService(context, InputMethodManager.class); + if (inputMethodManager == null) { + throw new IllegalStateException("No InputMethodManager service"); + } + + inputHandler = new LauncherProxyClient.InputHandler() { + @Override + public void updateState(TextInputState textInputState) { + post(() -> { + TextInputState prevState = inputState; + inputState = textInputState; + if (textInputState != null) { + setVisibility(VISIBLE); + setFocusable(true); + } + if (prevState == null && textInputState != null) { + setFocusableInTouchMode(true); + clearFocus(); + requestFocus(); + inputMethodManager.showSoftInput( + TouchControllerInputView.this, + InputMethodManager.SHOW_IMPLICIT + ); + } else if (prevState != null && textInputState == null) { + clearFocus(); + inputMethodManager.hideSoftInputFromWindow( + getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY + ); + } + if (textInputState != null) { + if (inputConnection != null) { + inputConnection.updateState(textInputState); + } + } else { + setVisibility(GONE); + setFocusable(false); + } + }); + } + + @Override + public void updateCursor(FloatRect cursorRect) { + TouchControllerInputView.this.cursorRect = cursorRect; + updateCursorAnchorInfo(); + } + + @Override + public void updateArea(FloatRect inputAreaRect) { + TouchControllerInputView.this.inputAreaRect = inputAreaRect; + updateCursorAnchorInfo(); + } + }; + } + + private static boolean isEmpty(TextRange range) { + return range.getLength() == 0; + } + + private static String removeRange(String text, TextRange range) { + return text.substring(0, range.getStart()) + text.substring(range.getEnd()); + } + + private static String substring(String text, TextRange range) { + return text.substring(range.getStart(), range.getEnd()); + } + + private static String replaceRange(String text, TextRange range, CharSequence newText) { + return text.substring(0, range.getStart()) + newText + text.substring(range.getEnd()); + } + + public LauncherProxyClient getClient() { + return client; + } + + public void setClient(LauncherProxyClient value) { + LauncherProxyClient prev = this.client; + if (prev != null) { + prev.setInputHandler(null); + } + this.client = value; + if (value != null) { + value.setInputHandler(inputHandler); + } + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Nullable + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + LauncherProxyClient currentClient = client; + TextInputState currentState = inputState; + if (currentClient == null || currentState == null) { + return null; + } + outAttrs.initialSelStart = currentState.getSelection().getStart(); + outAttrs.initialSelEnd = currentState.getSelection().getEnd(); + EditorInfoCompat.setInitialSurroundingText(outAttrs, currentState.getText()); + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL; + outAttrs.imeOptions = disableFullScreenInput ? EditorInfo.IME_FLAG_NO_FULLSCREEN : 0; + + inputConnection = new InputConnectionImpl(currentState, currentClient::updateTextInputState); + return inputConnection; + } + + private void updateCursorAnchorInfo() { + TextInputState inputState = this.inputState; + if (inputConnection == null || inputState == null) { + return; + } + CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); + builder.setSelectionRange(inputState.getSelection().getStart(), inputState.getSelection().getEnd()); + if (!isEmpty(inputState.getComposition())) { + builder.setComposingText(inputState.getComposition().getStart(), getCompositionText(inputState)); + } + if (cursorRect != null) { + builder.setInsertionMarkerLocation( + cursorRect.getLeft() * width, + cursorRect.getTop() * height, + (cursorRect.getLeft() + cursorRect.getWidth()) * width, + (cursorRect.getTop() + cursorRect.getHeight()) * height, + CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION + ); + } + if (inputAreaRect != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + EditorBoundsInfo editorBoundsInfo = new EditorBoundsInfo.Builder() + .setEditorBounds( + new RectF( + inputAreaRect.getLeft() * width, + inputAreaRect.getTop() * height, + (inputAreaRect.getLeft() + inputAreaRect.getWidth()) * width, + (inputAreaRect.getTop() + inputAreaRect.getHeight()) * height + ) + ) + .build(); + builder.setEditorBoundsInfo(editorBoundsInfo); + } + } + builder.setMatrix(getMatrix()); + inputMethodManager.updateCursorAnchorInfo(this, builder.build()); + } + + private class InputConnectionImpl implements InputConnection { + private TextInputState state; + private final StateChangedListener onStateChanged; + private final ClipboardManager clipboardManager; + private int inBatchEdit = 0; + private TextInputState delayedNewStateByBatchEdit; + private Integer extractTextToken; + private boolean hasZeroExtractToken = false; + + public InputConnectionImpl(TextInputState initialState, StateChangedListener onStateChanged) { + this.state = initialState; + this.onStateChanged = onStateChanged; + this.clipboardManager = ContextCompat.getSystemService(getContext(), ClipboardManager.class); + refreshState(); + } + + private TextRange minus(TextRange range1, TextRange range2) { + int e1 = range1.getEnd(); + int s2 = range2.getStart(); + int e2 = range2.getEnd(); + + int newStart = (range1.getStart() < s2) ? range1.getStart() : Math.max(range1.getStart(), e2) - range2.getLength(); + + int part1 = Math.min(e1, s2) - range1.getStart(); + int part2 = e1 - Math.max(range1.getStart(), e2); + + int newLength = Math.max(0, part1) + Math.max(0, part2); + return new TextRange(newStart, newLength); + } + + private void refreshState() { + inputMethodManager.updateSelection( + TouchControllerInputView.this, + state.getSelection().getStart(), + state.getSelection().getEnd(), + state.getComposition().getStart(), + state.getComposition().getEnd() + ); + ExtractedText extractedText = getExtractedText(); + if (hasZeroExtractToken) { + inputMethodManager.updateExtractedText(TouchControllerInputView.this, 0, extractedText); + } + if (extractTextToken != null) { + inputMethodManager.updateExtractedText(TouchControllerInputView.this, extractTextToken, extractedText); + } + } + + public void updateState(TextInputState newState) { + if (inBatchEdit > 0) { + delayedNewStateByBatchEdit = newState; + return; + } + if (state.equals(newState)) { + return; + } + if (!state.getText().equals(newState.getText())) { + inputMethodManager.restartInput(TouchControllerInputView.this); + } + state = newState; + refreshState(); + } + + private void updateState(TextInputStateUpdater updater) { + updateState(true, updater); + } + + private void updateState(boolean refresh, TextInputStateUpdater updater) { + TextInputState newState = updater.update(state); + state = newState; + if (inBatchEdit == 0) { + if (refresh) { + refreshState(); + } + onStateChanged.onStateChanged(newState); + } + } + + @Override + public boolean beginBatchEdit() { + inBatchEdit++; + return true; + } + + @Override + public boolean clearMetaKeyStates(int states) { + return true; + } + + @Override + public void closeConnection() { + } + + @Override + public boolean commitCompletion(CompletionInfo text) { + return true; + } + + @Override + public boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags, Bundle opts) { + return false; + } + + @Override + public boolean commitCorrection(CorrectionInfo correctionInfo) { + return false; + } + + private TextInputState commitTextAsNewState(TextInputState currentState, CharSequence text, int newCursorPosition) { + if (!isEmpty(currentState.getComposition())) { + String newText = replaceRange(currentState.getText(), currentState.getComposition(), text); + int finalCursorPosition = newCursorPosition > 0 ? + currentState.getComposition().getStart() + text.length() + newCursorPosition - 1 : + currentState.getComposition().getStart() - newCursorPosition; + finalCursorPosition = Math.max(0, Math.min(finalCursorPosition, newText.length())); + return new TextInputState( + newText, + TextRange.Companion.getEMPTY(), + new TextRange(finalCursorPosition), + currentState.getSelectionLeft() + ); + } else { + String newText = replaceRange(currentState.getText(), currentState.getSelection(), text); + int finalCursorPosition = newCursorPosition > 0 ? + currentState.getSelection().getStart() + text.length() + newCursorPosition - 1 : + currentState.getSelection().getStart() - newCursorPosition; + finalCursorPosition = Math.max(0, Math.min(finalCursorPosition, newText.length())); + return new TextInputState( + newText, + TextRange.Companion.getEMPTY(), + new TextRange(finalCursorPosition), + currentState.getSelectionLeft() + ); + } + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + int enterCount = 0; + StringBuilder filteredTextBuilder = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\n') { + enterCount++; + } else { + filteredTextBuilder.append(c); + } + } + CharSequence filteredText = filteredTextBuilder.toString(); + + if (filteredText.length() > 0) { + updateState(currentState -> commitTextAsNewState(currentState, filteredText, newCursorPosition)); + } + for (int i = 0; i < enterCount; i++) { + if (characterSenderStrategy != null) { + characterSenderStrategy.sendEnter(); + } + } + return true; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + updateState(currentState -> { + int limitedBeforeLength = Math.min(beforeLength, currentState.getSelection().getStart()); + int limitedAfterLength = Math.min(afterLength, currentState.getText().length() - currentState.getSelection().getEnd()); + String beforeText = currentState.getText().substring(0, currentState.getSelection().getStart() - limitedBeforeLength); + String selectedText = substring(currentState.getText(), currentState.getSelection()); + String afterText = currentState.getText().substring(currentState.getSelection().getEnd() + limitedAfterLength); + TextRange removedLeftRange = new TextRange(currentState.getSelection().getStart() - limitedBeforeLength, limitedBeforeLength); + TextRange removedRightRange = new TextRange(currentState.getSelection().getEnd(), limitedAfterLength); + return new TextInputState( + beforeText + selectedText + afterText, + minus(minus(currentState.getComposition(), removedRightRange), removedLeftRange), + new TextRange( + beforeText.length(), + selectedText.length() + ), + currentState.getSelectionLeft() + ); + }); + return true; + } + + @Override + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + TextInputState currentState = state; + String text = currentState.getText(); + int selectionStart = currentState.getSelection().getStart(); + + int remainingBefore = beforeLength; + int charCountBefore = 0; + int index = selectionStart - 1; + + while (remainingBefore > 0 && index >= 0) { + int codePoint = Character.codePointBefore(text, index + 1); + int charCount = Character.charCount(codePoint); + charCountBefore += charCount; + index -= charCount; + remainingBefore--; + } + + int remainingAfter = afterLength; + int charCountAfter = 0; + index = selectionStart; + + while (remainingAfter > 0 && index < text.length()) { + int codePoint = Character.codePointAt(text, index); + int charCount = Character.charCount(codePoint); + charCountAfter += charCount; + index += charCount; + remainingAfter--; + } + + return deleteSurroundingText(charCountBefore, charCountAfter); + } + + @Override + public boolean endBatchEdit() { + inBatchEdit--; + if (inBatchEdit == 0) { + if (delayedNewStateByBatchEdit != null) { + updateState(delayedNewStateByBatchEdit); + delayedNewStateByBatchEdit = null; + } else { + refreshState(); + onStateChanged.onStateChanged(state); + } + } + return inBatchEdit > 0; + } + + @Override + public boolean finishComposingText() { + updateState(currentState -> new TextInputState( + currentState.getText(), + TextRange.Companion.getEMPTY(), + currentState.getSelection(), + currentState.getSelectionLeft() + )); + return true; + } + + @Override + public int getCursorCapsMode(int reqModes) { + return state.getSelectionLeft() ? + TextUtils.getCapsMode(state.getText(), state.getSelection().getStart(), reqModes) : + TextUtils.getCapsMode(state.getText(), state.getSelection().getEnd(), reqModes); + } + + private ExtractedText getExtractedText() { + ExtractedText extractedText = new ExtractedText(); + extractedText.text = state.getText(); + extractedText.selectionStart = state.getSelection().getStart(); + extractedText.selectionEnd = state.getSelection().getEnd(); + extractedText.startOffset = 0; + extractedText.partialStartOffset = -1; + extractedText.partialEndOffset = 0; + return extractedText; + } + + @Override + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + if (request.token == 0) { + hasZeroExtractToken = true; + } else { + this.extractTextToken = request.token; + } + return getExtractedText(); + } + + @Nullable + @Override + public android.os.Handler getHandler() { + return null; + } + + @Nullable + @Override + public CharSequence getSelectedText(int flags) { + return !isEmpty(state.getSelection()) ? substring(state.getText(), state.getSelection()) : null; + } + + @Override + public CharSequence getTextAfterCursor(int n, int flags) { + int start = state.getSelection().getEnd(); + int end = Math.min(start + n, state.getText().length()); + return state.getText().substring(start, end); + } + + @Override + public CharSequence getTextBeforeCursor(int n, int flags) { + int end = state.getSelection().getStart(); + int start = Math.max(end - n, 0); + return state.getText().substring(start, end); + } + + @Override + public boolean performContextMenuAction(int id) { + switch (id) { + case android.R.id.selectAll: + updateState(state -> new TextInputState( + state.getText(), + new TextRange(0, state.getText().length()), + TextRange.Companion.getEMPTY(), + state.getSelectionLeft() + )); + break; + case android.R.id.cut: + CharSequence cutText = getSelectionText(state); + updateState(state -> new TextInputState( + removeRange(state.getText(), state.getSelection()), + new TextRange(state.getSelection().getStart()), + minus(state.getComposition(), state.getSelection()), + state.getSelectionLeft() + )); + if (clipboardManager != null) { + clipboardManager.setPrimaryClip(ClipData.newPlainText(null, cutText)); + } + break; + case android.R.id.copy: + if (clipboardManager != null) { + clipboardManager.setPrimaryClip( + ClipData.newPlainText( + null, + getSelectionText(state) + ) + ); + } + break; + case android.R.id.paste: + if (clipboardManager != null && clipboardManager.getPrimaryClip() != null && clipboardManager.getPrimaryClip().getItemCount() > 0) { + CharSequence text = clipboardManager.getPrimaryClip().getItemAt(0).getText(); + if (text != null) { + updateState(state -> commitTextAsNewState(state, text, 1)); + } + } + break; + default: + return false; + } + return true; + } + + @Override + public boolean performEditorAction(int editorAction) { + return false; + } + + @Override + public boolean performPrivateCommand(String action, Bundle data) { + return false; + } + + @Override + public boolean reportFullscreenMode(boolean enabled) { + if (!inputMethodManager.isFullscreenMode()) { + extractTextToken = null; + } + return true; + } + + @Override + public boolean requestCursorUpdates(int cursorUpdateMode) { + return false; + } + + @Override + public boolean sendKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return true; + } + if (characterSenderStrategy != null) { + characterSenderStrategy.sendEnter(); + } + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return true; + } + if (event.isShiftPressed()) { + updateState(TextInputStateKt::doShiftLeft); + } else { + updateState(TextInputStateKt::doArrowLeft); + } + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return true; + } + if (event.isShiftPressed()) { + updateState(TextInputStateKt::doShiftRight); + } else { + updateState(TextInputStateKt::doArrowRight); + } + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return true; + } + updateState(TextInputStateKt::doBackspace); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return true; + } + updateState(TextInputStateKt::doDelete); + } else { + int index = EfficientAndroidLWJGLKeycode.getIndexByKey(event.getKeyCode()); + if (EfficientAndroidLWJGLKeycode.containsIndex(index)) { + EfficientAndroidLWJGLKeycode.execKey(event, index); + } + } + return true; + } + + @Override + public boolean setComposingRegion(int start, int end) { + updateState(false, currentState -> new TextInputState( + currentState.getText(), + new TextRange(start, end - start), + currentState.getSelection(), + currentState.getSelectionLeft() + )); + return true; + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + updateState(currentState -> { + if (!isEmpty(currentState.getComposition())) { + String newText = replaceRange(currentState.getText(), currentState.getComposition(), text); + int finalCursorPosition = newCursorPosition > 0 ? + currentState.getComposition().getStart() + text.length() + newCursorPosition - 1 : + currentState.getComposition().getStart() - newCursorPosition; + finalCursorPosition = Math.max(0, Math.min(finalCursorPosition, newText.length())); + return new TextInputState( + newText, + new TextRange(currentState.getComposition().getStart(), text.length()), + new TextRange(finalCursorPosition), + currentState.getSelectionLeft() + ); + } else { + String newText = replaceRange(currentState.getText(), currentState.getSelection(), text); + int finalCursorPosition = newCursorPosition > 0 ? + currentState.getSelection().getStart() + text.length() + newCursorPosition - 1 : + currentState.getSelection().getStart() - newCursorPosition; + finalCursorPosition = Math.max(0, Math.min(finalCursorPosition, newText.length())); + return new TextInputState( + newText, + new TextRange(currentState.getSelection().getStart(), text.length()), + new TextRange(finalCursorPosition), + currentState.getSelectionLeft() + ); + } + }); + return true; + } + + @Override + public boolean setSelection(int start, int end) { + updateState(currentState -> new TextInputState( + currentState.getText(), + currentState.getComposition(), + new TextRange(start, end - start), + currentState.getSelectionLeft() + )); + return true; + } + } + + private interface StateChangedListener { + void onStateChanged(TextInputState newState); + } + + private interface TextInputStateUpdater { + TextInputState update(TextInputState currentState); + } +} \ No newline at end of file diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerUtils.java index c999a283e3..fa3a3b60a4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/TouchControllerUtils.java @@ -5,6 +5,7 @@ import top.fifthlight.touchcontroller.proxy.client.LauncherProxyClient; import top.fifthlight.touchcontroller.proxy.client.MessageTransport; +import top.fifthlight.touchcontroller.proxy.client.PlatformCapability; import top.fifthlight.touchcontroller.proxy.client.android.transport.UnixSocketTransportKt; import top.fifthlight.touchcontroller.proxy.message.VibrateMessage; @@ -20,6 +21,8 @@ import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import java.util.Set; + public class TouchControllerUtils { private TouchControllerUtils() { } @@ -93,7 +96,7 @@ public static void processTouchEvent(MotionEvent motionEvent, View view) { } } - public static void initialize(Context context) { + public static void initialize(Context context, TouchControllerInputView touchControllerInputView) { if (proxyClient != null) { return; } @@ -103,8 +106,9 @@ public static void initialize(Context context) { Log.w("TouchController", "Failed to set TouchController environment variable", e); } MessageTransport transport = UnixSocketTransportKt.UnixSocketTransport(socketName); - proxyClient = new LauncherProxyClient(transport); + proxyClient = new LauncherProxyClient(transport, Set.of(PlatformCapability.TEXT_STATUS)); proxyClient.run(); + touchControllerInputView.setClient(proxyClient); Vibrator vibrator = ContextCompat.getSystemService(context, Vibrator.class); if (vibrator != null) { LauncherProxyClient.VibrationHandler vibrationHandler = new VibrationHandler(vibrator); diff --git a/app_pojavlauncher/src/main/res/layout/activity_basemain.xml b/app_pojavlauncher/src/main/res/layout/activity_basemain.xml index bd7f5f7a15..ac34158ccc 100644 --- a/app_pojavlauncher/src/main/res/layout/activity_basemain.xml +++ b/app_pojavlauncher/src/main/res/layout/activity_basemain.xml @@ -49,6 +49,11 @@ android:inputType="textFilter|textImeMultiLine|textAutoComplete|textAutoCorrect" tools:ignore="TouchTargetSizeCheck" /> + +