mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-07 22:04:03 -05:00
app: copy GameTextInput java code into app
This commit is contained in:
@@ -64,22 +64,5 @@ fn main() {
|
||||
"cargo:warning='leaving linker args for {target_os}:{target_arch} unchanged"
|
||||
),
|
||||
}
|
||||
|
||||
// Link GameTextInput static library
|
||||
let gametextinput_lib_dir = match target_arch.as_str() {
|
||||
"aarch64" => "pkg/android/libs/android.arm64-v8a",
|
||||
"arm" => "pkg/android/libs/android.armeabi-v7a",
|
||||
"i686" => "pkg/android/libs/android.x86",
|
||||
"x86_64" => "pkg/android/libs/android.x86_64",
|
||||
_ => {
|
||||
println!("cargo:warning=Unknown target arch for GameTextInput: {target_arch}");
|
||||
"pkg/android/libs"
|
||||
}
|
||||
};
|
||||
println!("cargo:rustc-link-search=native={gametextinput_lib_dir}");
|
||||
println!("cargo:rustc-link-lib=static=game-text-input");
|
||||
|
||||
// Rebuild if headers change
|
||||
println!("cargo:rerun-if-changed=src/android/textinput/include");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets.Type;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.text.InputType;
|
||||
import java.util.HashMap;
|
||||
|
||||
import videodecode.VideoDecoder;
|
||||
import textinput.InputConnection;
|
||||
import textinput.Settings;
|
||||
import textinput.Listener;
|
||||
import textinput.State;
|
||||
import textinput.GameTextInput;
|
||||
|
||||
//% END
|
||||
|
||||
@@ -41,6 +48,13 @@ if (true)
|
||||
|
||||
//% MAIN_ACTIVITY_BODY
|
||||
|
||||
// GameTextInput native bridge functions (following official Android pattern)
|
||||
native void setInputConnectionNative(textinput.InputConnection c);
|
||||
native void onTextInputEventNative(textinput.State softKeyboardEvent);
|
||||
|
||||
// GameTextInput InputConnection reference (public for QuadSurface access)
|
||||
public textinput.InputConnection gameTextInputInputConnection;
|
||||
|
||||
public String getAppDataPath() {
|
||||
return getApplicationContext().getDataDir().getAbsolutePath();
|
||||
}
|
||||
@@ -81,9 +95,9 @@ public VideoDecoder createVideoDecoder() {
|
||||
|
||||
//% MAIN_ACTIVITY_ON_CREATE
|
||||
|
||||
view.setFocusable(false);
|
||||
view.setFocusableInTouchMode(false);
|
||||
view.clearFocus();
|
||||
//view.setFocusable(false);
|
||||
//view.setFocusableInTouchMode(false);
|
||||
//view.clearFocus();
|
||||
|
||||
// Start a foreground service so the app stays awake
|
||||
Intent serviceIntent = new Intent(this, ForegroundService.class);
|
||||
@@ -91,3 +105,79 @@ startForegroundService(serviceIntent);
|
||||
|
||||
//% END
|
||||
|
||||
|
||||
//% QUAD_SURFACE_ON_CREATE_INPUT_CONNECTION
|
||||
|
||||
// Get reference to MainActivity
|
||||
if (getContext() == null)
|
||||
Log.i("darkfi", "getCTX (on creat) is nulllll!!!!!!!!!!!!!!!!!!");
|
||||
MainActivity mainActivity = (MainActivity)getContext();
|
||||
|
||||
android.util.Log.d("darkfi", "onCreateInputConnection called");
|
||||
|
||||
// Create InputConnection if it doesn't exist yet
|
||||
if (mainActivity.gameTextInputInputConnection == null) {
|
||||
android.util.Log.d("darkfi", "Creating new InputConnection");
|
||||
// Create InputConnection with Context (from QuadSurface)
|
||||
android.view.inputmethod.EditorInfo editorInfo = new android.view.inputmethod.EditorInfo();
|
||||
editorInfo.inputType = android.text.InputType.TYPE_CLASS_TEXT |
|
||||
android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
|
||||
editorInfo.imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
|
||||
if (mainActivity == null)
|
||||
Log.i("darkfi", "mainact is NULLLL");
|
||||
mainActivity.gameTextInputInputConnection = new textinput.InputConnection(
|
||||
getContext(),
|
||||
this,
|
||||
new textinput.Settings(editorInfo, true)
|
||||
);
|
||||
|
||||
// Pass the InputConnection to native GameTextInput library
|
||||
android.util.Log.d("darkfi", "InputConnection created and passed to native");
|
||||
mainActivity.setInputConnectionNative(mainActivity.gameTextInputInputConnection);
|
||||
} else {
|
||||
android.util.Log.d("darkfi", "Reusing existing InputConnection");
|
||||
}
|
||||
|
||||
// Set the listener to receive IME state changes
|
||||
mainActivity.gameTextInputInputConnection.setListener(new textinput.Listener() {
|
||||
@Override
|
||||
public void stateChanged(textinput.State newState, boolean dismissed) {
|
||||
// Called when the IME sends new text state
|
||||
// Forward to native code which triggers Rust callback
|
||||
android.util.Log.d("darkfi", "stateChanged: text=" + newState.toString());
|
||||
mainActivity.onTextInputEventNative(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImeInsetsChanged(androidx.core.graphics.Insets insets) {
|
||||
// Called when IME insets change (e.g., keyboard height changes)
|
||||
// Optional: can be used for dynamic layout adjustment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoftwareKeyboardVisibilityChanged(boolean visible) {
|
||||
// Called when keyboard is shown or hidden
|
||||
android.util.Log.d("darkfi", "onSoftwareKeyboardVisibilityChanged: " + visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditorAction(int actionCode) {
|
||||
// Called when user presses action button (Done, Next, etc.)
|
||||
// Optional: handle specific editor actions
|
||||
}
|
||||
});
|
||||
|
||||
// Copy EditorInfo from GameTextInput to configure IME
|
||||
if (outAttrs != null) {
|
||||
textinput.GameTextInput.copyEditorInfo(
|
||||
mainActivity.gameTextInputInputConnection.getEditorInfo(),
|
||||
outAttrs
|
||||
);
|
||||
}
|
||||
|
||||
// Return the GameTextInput InputConnection to IME
|
||||
if (true) return mainActivity.gameTextInputInputConnection;
|
||||
return mainActivity.gameTextInputInputConnection;
|
||||
|
||||
//% END
|
||||
|
||||
52
bin/app/java/textinput/GameTextInput.java
Normal file
52
bin/app/java/textinput/GameTextInput.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
/*
|
||||
* Singleton GameTextInput class with helper methods.
|
||||
*/
|
||||
public final class GameTextInput {
|
||||
public final static void copyEditorInfo(EditorInfo from, EditorInfo to) {
|
||||
if (from == null || to == null)
|
||||
return;
|
||||
if (from.hintText != null) {
|
||||
to.hintText = from.hintText;
|
||||
}
|
||||
|
||||
to.inputType = from.inputType;
|
||||
to.imeOptions = from.imeOptions;
|
||||
to.label = from.label;
|
||||
to.initialCapsMode = from.initialCapsMode;
|
||||
to.privateImeOptions = from.privateImeOptions;
|
||||
if (from.packageName != null) {
|
||||
to.packageName = from.packageName;
|
||||
}
|
||||
|
||||
to.fieldId = from.fieldId;
|
||||
if (from.fieldName != null) {
|
||||
to.fieldName = from.fieldName;
|
||||
}
|
||||
|
||||
to.initialSelStart = from.initialSelStart;
|
||||
to.initialSelEnd = from.initialSelEnd;
|
||||
}
|
||||
|
||||
private GameTextInput() {}
|
||||
}
|
||||
711
bin/app/java/textinput/InputConnection.java
Normal file
711
bin/app/java/textinput/InputConnection.java
Normal file
@@ -0,0 +1,711 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_UNSPECIFIED;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Selection;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.CompletionInfo;
|
||||
import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
public class InputConnection extends BaseInputConnection implements View.OnKeyListener {
|
||||
private static final String TAG = "gti.InputConnection";
|
||||
private final InputMethodManager imm;
|
||||
private final View targetView;
|
||||
private final Settings settings;
|
||||
private final Editable mEditable;
|
||||
private Listener listener;
|
||||
private boolean mSoftKeyboardActive;
|
||||
|
||||
/*
|
||||
* This class filters EOL characters from the input. For details of how InputFilter.filter
|
||||
* function works, refer to its documentation. If the suggested change is accepted without
|
||||
* modifications, filter() should return null.
|
||||
*/
|
||||
private class SingeLineFilter implements InputFilter {
|
||||
public CharSequence filter(
|
||||
CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
boolean keepOriginal = true;
|
||||
StringBuilder builder = new StringBuilder(end - start);
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
char c = source.charAt(i);
|
||||
|
||||
if (c == '\n') {
|
||||
keepOriginal = false;
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (keepOriginal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source instanceof Spanned) {
|
||||
SpannableString s = new SpannableString(builder);
|
||||
TextUtils.copySpansFrom((Spanned) source, start, builder.length(), null, s, 0);
|
||||
return s;
|
||||
} else {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT = 5000;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param ctx The app's context
|
||||
* @param targetView The view created this input connection
|
||||
* @param settings EditorInfo and other settings needed by this class
|
||||
* InputConnection.
|
||||
*/
|
||||
public InputConnection(Context ctx, View targetView, Settings settings) {
|
||||
super(targetView, settings.mEditorInfo.inputType != 0);
|
||||
Log.d(TAG, "InputConnection created");
|
||||
|
||||
this.targetView = targetView;
|
||||
this.settings = settings;
|
||||
Object imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm == null) {
|
||||
throw new java.lang.RuntimeException("Can't get IMM");
|
||||
} else {
|
||||
this.imm = (InputMethodManager) imm;
|
||||
this.mEditable = (Editable) (new SpannableStringBuilder());
|
||||
}
|
||||
// Listen for insets changes
|
||||
WindowCompat.setDecorFitsSystemWindows(((Activity) targetView.getContext()).getWindow(), false);
|
||||
targetView.setOnKeyListener(this);
|
||||
// Apply EditorInfo settings
|
||||
this.setEditorInfo(settings.mEditorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the input method manager. This is useful to apply changes to the keyboard
|
||||
* after calling setEditorInfo.
|
||||
*/
|
||||
public void restartInput() {
|
||||
imm.restartInput(targetView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the soft keyboard is visible.
|
||||
*
|
||||
* @return true if the soft keyboard is visible, false otherwise
|
||||
*/
|
||||
public final boolean getSoftKeyboardActive() {
|
||||
return this.mSoftKeyboardActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the soft keyboard to become visible or invisible.
|
||||
*
|
||||
* @param active True if the soft keyboard should be made visible, otherwise false.
|
||||
* @param flags See
|
||||
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#showSoftInput(android.view.View,%20int)
|
||||
*/
|
||||
public final void setSoftKeyboardActive(boolean active, int flags) {
|
||||
Log.d(TAG, "setSoftKeyboardActive, active: " + active);
|
||||
|
||||
this.mSoftKeyboardActive = active;
|
||||
if (active) {
|
||||
this.targetView.setFocusableInTouchMode(true);
|
||||
this.targetView.requestFocus();
|
||||
this.imm.showSoftInput(this.targetView, flags);
|
||||
} else {
|
||||
this.imm.hideSoftInputFromWindow(this.targetView.getWindowToken(), flags);
|
||||
}
|
||||
restartInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current EditorInfo used to configure the InputConnection's behaviour.
|
||||
*
|
||||
* @return The current EditorInfo.
|
||||
*/
|
||||
public final EditorInfo getEditorInfo() {
|
||||
return this.settings.mEditorInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current EditorInfo used to configure the InputConnection's behaviour.
|
||||
*
|
||||
* @param editorInfo The EditorInfo to use
|
||||
*/
|
||||
public final void setEditorInfo(EditorInfo editorInfo) {
|
||||
Log.d(TAG, "setEditorInfo");
|
||||
settings.mEditorInfo = editorInfo;
|
||||
|
||||
// Depending on the multiline state, we might need a different set of filters.
|
||||
// Filters are being used to filter specific characters for hardware keyboards
|
||||
// (software input methods already support TYPE_TEXT_FLAG_MULTI_LINE).
|
||||
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
|
||||
mEditable.setFilters(
|
||||
new InputFilter[] {new InputFilter.LengthFilter(MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT),
|
||||
new SingeLineFilter()});
|
||||
} else {
|
||||
mEditable.setFilters(new InputFilter[] {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text, selection and composing region state.
|
||||
*
|
||||
* @param state The state to be used by the IME.
|
||||
* This replaces any text, selections and composing regions currently active.
|
||||
*/
|
||||
public final void setState(State state) {
|
||||
if (state == null)
|
||||
return;
|
||||
Log.d(TAG,
|
||||
"setState: '" + state.text + "', selection=(" + state.selectionStart + ","
|
||||
+ state.selectionEnd + "), composing region=(" + state.composingRegionStart + ","
|
||||
+ state.composingRegionEnd + ")");
|
||||
mEditable.clear();
|
||||
mEditable.clearSpans();
|
||||
mEditable.insert(0, (CharSequence) state.text);
|
||||
setSelection(state.selectionStart, state.selectionEnd);
|
||||
if (state.composingRegionStart != state.composingRegionEnd) {
|
||||
setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
|
||||
}
|
||||
restartInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current listener for state changes.
|
||||
*
|
||||
* @return The current Listener
|
||||
*/
|
||||
public final Listener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener for state changes.
|
||||
*
|
||||
* @param listener
|
||||
* @return This InputConnection, for setter chaining.
|
||||
*/
|
||||
public final InputConnection setListener(Listener listener) {
|
||||
this.listener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
// From View.OnKeyListener
|
||||
@Override
|
||||
public boolean onKey(View view, int i, KeyEvent keyEvent) {
|
||||
Log.d(TAG, "onKey: " + keyEvent);
|
||||
if (!getSoftKeyboardActive()) {
|
||||
return false;
|
||||
}
|
||||
// Don't call sendKeyEvent as it might produce an infinite loop.
|
||||
if (processKeyEvent(keyEvent)) {
|
||||
// IMM seems to cache the content of Editable, so we update it with restartInput
|
||||
// Also it caches selection and composing region, so let's notify it about updates.
|
||||
stateUpdated();
|
||||
immUpdateSelection();
|
||||
restartInput();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
Log.d(TAG, "getEditable");
|
||||
return mEditable;
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean setSelection(int start, int end) {
|
||||
Log.d(TAG, "setSelection: " + start + ":" + end);
|
||||
return super.setSelection(start, end);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
Log.d(
|
||||
TAG, String.format("setComposingText='%s' newCursorPosition=%d", text, newCursorPosition));
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
return super.setComposingText(text, newCursorPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingRegion(int start, int end) {
|
||||
Log.d(TAG, "setComposingRegion: " + start + ":" + end);
|
||||
return super.setComposingRegion(start, end);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean finishComposingText() {
|
||||
Log.d(TAG, "finishComposingText");
|
||||
return super.finishComposingText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean endBatchEdit() {
|
||||
Log.d(TAG, "endBatchEdit");
|
||||
stateUpdated();
|
||||
return super.endBatchEdit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitCompletion(CompletionInfo text) {
|
||||
Log.d(TAG, "commitCompletion");
|
||||
return super.commitCompletion(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitCorrection(CorrectionInfo text) {
|
||||
Log.d(TAG, "commitCompletion");
|
||||
return super.commitCorrection(text);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
Log.d(TAG,
|
||||
(new StringBuilder())
|
||||
.append("commitText: ")
|
||||
.append(text)
|
||||
.append(", new pos = ")
|
||||
.append(newCursorPosition)
|
||||
.toString());
|
||||
return super.commitText(text, newCursorPosition);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
Log.d(TAG, "deleteSurroundingText: " + beforeLength + ":" + afterLength);
|
||||
return super.deleteSurroundingText(beforeLength, afterLength);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
|
||||
Log.d(TAG, "deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength);
|
||||
return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
Log.d(TAG, "sendKeyEvent: " + event);
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public CharSequence getSelectedText(int flags) {
|
||||
CharSequence result = super.getSelectedText(flags);
|
||||
if (result == null) {
|
||||
result = "";
|
||||
}
|
||||
Log.d(TAG, "getSelectedText: " + flags + ", result: " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public CharSequence getTextAfterCursor(int length, int flags) {
|
||||
Log.d(TAG, "getTextAfterCursor: " + length + ":" + flags);
|
||||
if (length < 0) {
|
||||
Log.i(TAG, "getTextAfterCursor: returning null to due to an invalid length=" + length);
|
||||
return null;
|
||||
}
|
||||
return super.getTextAfterCursor(length, flags);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public CharSequence getTextBeforeCursor(int length, int flags) {
|
||||
Log.d(TAG, "getTextBeforeCursor: " + length + ", flags=" + flags);
|
||||
if (length < 0) {
|
||||
Log.i(TAG, "getTextBeforeCursor: returning null to due to an invalid length=" + length);
|
||||
return null;
|
||||
}
|
||||
return super.getTextBeforeCursor(length, flags);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public boolean requestCursorUpdates(int cursorUpdateMode) {
|
||||
Log.d(TAG, "Request cursor updates: " + cursorUpdateMode);
|
||||
return super.requestCursorUpdates(cursorUpdateMode);
|
||||
}
|
||||
|
||||
// From BaseInputConnection
|
||||
@Override
|
||||
public void closeConnection() {
|
||||
Log.d(TAG, "closeConnection");
|
||||
super.closeConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setImeConsumesInput(boolean imeConsumesInput) {
|
||||
Log.d(TAG, "setImeConsumesInput: " + imeConsumesInput);
|
||||
return super.setImeConsumesInput(imeConsumesInput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
||||
Log.d(TAG, "getExtractedText");
|
||||
return super.getExtractedText(request, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performPrivateCommand(String action, Bundle data) {
|
||||
Log.d(TAG, "performPrivateCommand");
|
||||
return super.performPrivateCommand(action, data);
|
||||
}
|
||||
|
||||
private void immUpdateSelection() {
|
||||
Pair selection = this.getSelection();
|
||||
Pair cr = this.getComposingRegion();
|
||||
Log.d(TAG,
|
||||
"immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + ","
|
||||
+ cr.second);
|
||||
settings.mEditorInfo.initialSelStart = selection.first;
|
||||
settings.mEditorInfo.initialSelEnd = selection.second;
|
||||
imm.updateSelection(targetView, selection.first, selection.second, cr.first, cr.second);
|
||||
}
|
||||
|
||||
private Pair getSelection() {
|
||||
return new Pair(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
|
||||
}
|
||||
|
||||
private Pair getComposingRegion() {
|
||||
return new Pair(getComposingSpanStart(mEditable), getComposingSpanEnd(mEditable));
|
||||
}
|
||||
|
||||
private boolean processKeyEvent(KeyEvent event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
int keyCode = event.getKeyCode();
|
||||
Log.d(
|
||||
TAG, String.format("processKeyEvent(key=%d) text=%s", keyCode, this.mEditable.toString()));
|
||||
// Filter out Enter keys if multi-line mode is disabled.
|
||||
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0
|
||||
&& (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
|
||||
&& event.hasNoModifiers()) {
|
||||
sendEditorAction(settings.mEditorInfo.actionId);
|
||||
return true;
|
||||
}
|
||||
if (event.getAction() != KeyEvent.ACTION_DOWN) {
|
||||
return false;
|
||||
}
|
||||
// If no selection is set, move the selection to the end.
|
||||
// This is the case when first typing on keys when the selection is not set.
|
||||
// Note that for InputType.TYPE_CLASS_TEXT, this is not be needed because the
|
||||
// selection is set in setComposingText.
|
||||
Pair selection = this.getSelection();
|
||||
if (selection.first == -1) {
|
||||
selection.first = this.mEditable.length();
|
||||
selection.second = this.mEditable.length();
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
if (selection.first == selection.second) {
|
||||
int newIndex = findIndexBackward(mEditable, selection.first, 1);
|
||||
setSelection(newIndex, newIndex);
|
||||
} else {
|
||||
setSelection(selection.first, selection.first);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
if (selection.first == selection.second) {
|
||||
int newIndex = findIndexForward(mEditable, selection.second, 1);
|
||||
setSelection(newIndex, newIndex);
|
||||
} else {
|
||||
setSelection(selection.second, selection.second);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
|
||||
setSelection(0, 0);
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
|
||||
setSelection(this.mEditable.length(), this.mEditable.length());
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
|
||||
if (selection.first != selection.second) {
|
||||
this.mEditable.delete(selection.first, selection.second);
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_DEL) {
|
||||
if (selection.first > 0) {
|
||||
finishComposingText();
|
||||
deleteSurroundingTextInCodePoints(1, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
|
||||
if (selection.first < this.mEditable.length()) {
|
||||
finishComposingText();
|
||||
deleteSurroundingTextInCodePoints(0, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.getUnicodeChar() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selection.first != selection.second) {
|
||||
Log.d(TAG, String.format("processKeyEvent: deleting selection"));
|
||||
this.mEditable.delete(selection.first, selection.second);
|
||||
}
|
||||
|
||||
String charsToInsert = Character.toString((char) event.getUnicodeChar());
|
||||
this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
|
||||
int length = this.mEditable.length();
|
||||
|
||||
// Same logic as in setComposingText(): we must update composing region,
|
||||
// so make sure it points to a valid range.
|
||||
Pair composingRegion = this.getComposingRegion();
|
||||
if (composingRegion.first == -1) {
|
||||
composingRegion = this.getSelection();
|
||||
if (composingRegion.first == -1) {
|
||||
composingRegion = new Pair(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
composingRegion.second = composingRegion.first + length;
|
||||
this.setComposingRegion(composingRegion.first, composingRegion.second);
|
||||
int new_cursor = selection.first + charsToInsert.length();
|
||||
setSelection(new_cursor, new_cursor);
|
||||
Log.d(TAG, String.format("processKeyEvent: exit, text=%s", this.mEditable.toString()));
|
||||
return true;
|
||||
}
|
||||
|
||||
private final void stateUpdated() {
|
||||
Pair selection = this.getSelection();
|
||||
Pair cr = this.getComposingRegion();
|
||||
State state = new State(
|
||||
this.mEditable.toString(), selection.first, selection.second, cr.first, cr.second);
|
||||
settings.mEditorInfo.initialSelStart = selection.first;
|
||||
settings.mEditorInfo.initialSelEnd = selection.second;
|
||||
|
||||
// Keep a reference to the listener to avoid a race condition when setting the listener.
|
||||
Listener listener = this.listener;
|
||||
|
||||
// We always propagate state change events because unfortunately keyboard visibility functions
|
||||
// are unreliable, and text editor logic should not depend on them.
|
||||
if (listener != null) {
|
||||
listener.stateChanged(state, /*dismissed=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current IME insets.
|
||||
*
|
||||
* @return The current IME insets
|
||||
*/
|
||||
public Insets getImeInsets() {
|
||||
if (this.targetView == null) {
|
||||
return Insets.NONE;
|
||||
}
|
||||
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this.targetView);
|
||||
|
||||
if (insets == null) {
|
||||
return Insets.NONE;
|
||||
}
|
||||
|
||||
return insets.getInsets(WindowInsetsCompat.Type.ime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if software keyboard is visible, false otherwise.
|
||||
*
|
||||
* @return whether software IME is visible or not.
|
||||
*/
|
||||
public boolean isSoftwareKeyboardVisible() {
|
||||
if (this.targetView == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this.targetView);
|
||||
|
||||
if (insets == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return insets.isVisible(WindowInsetsCompat.Type.ime());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an event handler from InputConnection interface.
|
||||
* It's called when action button is triggered (typically this means Enter was pressed).
|
||||
*
|
||||
* @param action Action code, either one from EditorInfo.imeOptions or a custom one.
|
||||
* @return Returns true on success, false if the input connection is no longer valid.
|
||||
*/
|
||||
@Override
|
||||
public boolean performEditorAction(int action) {
|
||||
Log.d(TAG, "performEditorAction, action=" + action);
|
||||
if (action == IME_ACTION_UNSPECIFIED) {
|
||||
// Super emulates Enter key press/release
|
||||
return super.performEditorAction(action);
|
||||
}
|
||||
return sendEditorAction(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers editor action to listener
|
||||
*
|
||||
* @param action Action code, either one from EditorInfo.imeOptions or a custom one.
|
||||
* @return Returns true on success, false if the input connection is no longer valid.
|
||||
*/
|
||||
private boolean sendEditorAction(int action) {
|
||||
Listener listener = this.listener;
|
||||
if (listener != null) {
|
||||
listener.onEditorAction(action);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int INVALID_INDEX = -1;
|
||||
// Implementation copy from BaseInputConnection
|
||||
private static int findIndexBackward(
|
||||
final CharSequence cs, final int from, final int numCodePoints) {
|
||||
int currentIndex = from;
|
||||
boolean waitingHighSurrogate = false;
|
||||
final int N = cs.length();
|
||||
if (currentIndex < 0 || N < currentIndex) {
|
||||
return INVALID_INDEX; // The starting point is out of range.
|
||||
}
|
||||
if (numCodePoints < 0) {
|
||||
return INVALID_INDEX; // Basically this should not happen.
|
||||
}
|
||||
int remainingCodePoints = numCodePoints;
|
||||
while (true) {
|
||||
if (remainingCodePoints == 0) {
|
||||
return currentIndex; // Reached to the requested length in code points.
|
||||
}
|
||||
|
||||
--currentIndex;
|
||||
if (currentIndex < 0) {
|
||||
if (waitingHighSurrogate) {
|
||||
return INVALID_INDEX; // An invalid surrogate pair is found.
|
||||
}
|
||||
return 0; // Reached to the beginning of the text w/o any invalid surrogate pair.
|
||||
}
|
||||
final char c = cs.charAt(currentIndex);
|
||||
if (waitingHighSurrogate) {
|
||||
if (!java.lang.Character.isHighSurrogate(c)) {
|
||||
return INVALID_INDEX; // An invalid surrogate pair is found.
|
||||
}
|
||||
waitingHighSurrogate = false;
|
||||
--remainingCodePoints;
|
||||
continue;
|
||||
}
|
||||
if (!java.lang.Character.isSurrogate(c)) {
|
||||
--remainingCodePoints;
|
||||
continue;
|
||||
}
|
||||
if (java.lang.Character.isHighSurrogate(c)) {
|
||||
return INVALID_INDEX; // A invalid surrogate pair is found.
|
||||
}
|
||||
waitingHighSurrogate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation copy from BaseInputConnection
|
||||
private static int findIndexForward(
|
||||
final CharSequence cs, final int from, final int numCodePoints) {
|
||||
int currentIndex = from;
|
||||
boolean waitingLowSurrogate = false;
|
||||
final int N = cs.length();
|
||||
if (currentIndex < 0 || N < currentIndex) {
|
||||
return INVALID_INDEX; // The starting point is out of range.
|
||||
}
|
||||
if (numCodePoints < 0) {
|
||||
return INVALID_INDEX; // Basically this should not happen.
|
||||
}
|
||||
int remainingCodePoints = numCodePoints;
|
||||
|
||||
while (true) {
|
||||
if (remainingCodePoints == 0) {
|
||||
return currentIndex; // Reached to the requested length in code points.
|
||||
}
|
||||
|
||||
if (currentIndex >= N) {
|
||||
if (waitingLowSurrogate) {
|
||||
return INVALID_INDEX; // An invalid surrogate pair is found.
|
||||
}
|
||||
return N; // Reached to the end of the text w/o any invalid surrogate pair.
|
||||
}
|
||||
final char c = cs.charAt(currentIndex);
|
||||
if (waitingLowSurrogate) {
|
||||
if (!java.lang.Character.isLowSurrogate(c)) {
|
||||
return INVALID_INDEX; // An invalid surrogate pair is found.
|
||||
}
|
||||
--remainingCodePoints;
|
||||
waitingLowSurrogate = false;
|
||||
++currentIndex;
|
||||
continue;
|
||||
}
|
||||
if (!java.lang.Character.isSurrogate(c)) {
|
||||
--remainingCodePoints;
|
||||
++currentIndex;
|
||||
continue;
|
||||
}
|
||||
if (java.lang.Character.isLowSurrogate(c)) {
|
||||
return INVALID_INDEX; // A invalid surrogate pair is found.
|
||||
}
|
||||
waitingLowSurrogate = true;
|
||||
++currentIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
bin/app/java/textinput/Listener.java
Normal file
55
bin/app/java/textinput/Listener.java
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
|
||||
/**
|
||||
* Listener interface for text, selection and composing region changes.
|
||||
* Also a listener for window insets changes.
|
||||
*/
|
||||
public interface Listener {
|
||||
/*
|
||||
* Called when the IME text, selection or composing region has changed.
|
||||
*
|
||||
* @param newState The updated state
|
||||
* @param dismmissed Deprecated, don't use
|
||||
*/
|
||||
void stateChanged(State newState, boolean dismissed);
|
||||
|
||||
/*
|
||||
* Called when the IME window insets change, i.e. the IME moves into or out of view.
|
||||
*
|
||||
* @param insets The new window insets, i.e. the offsets of top, bottom, left and right
|
||||
* relative to the window
|
||||
*/
|
||||
void onImeInsetsChanged(Insets insets);
|
||||
|
||||
/*
|
||||
* Called when the IME window is shown or hidden.
|
||||
*
|
||||
* @param insets True is IME is visible, false otherwise.
|
||||
*/
|
||||
void onSoftwareKeyboardVisibilityChanged(boolean visible);
|
||||
|
||||
/*
|
||||
* Called when any editor action is performed. Typically this means that
|
||||
* the Enter button has been pressed.
|
||||
*
|
||||
* @param action Code of the action. A default action is IME_ACTION_DONE.
|
||||
*/
|
||||
void onEditorAction(int action);
|
||||
}
|
||||
25
bin/app/java/textinput/Pair.java
Normal file
25
bin/app/java/textinput/Pair.java
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
public final class Pair {
|
||||
public int first, second;
|
||||
|
||||
public Pair(int f, int s) {
|
||||
first = f;
|
||||
second = s;
|
||||
}
|
||||
}
|
||||
29
bin/app/java/textinput/Settings.java
Normal file
29
bin/app/java/textinput/Settings.java
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
// Settings for InputConnection
|
||||
public final class Settings {
|
||||
EditorInfo mEditorInfo;
|
||||
boolean mForwardKeyEvents;
|
||||
|
||||
public Settings(EditorInfo editorInfo, boolean forwardKeyEvents) {
|
||||
mEditorInfo = editorInfo;
|
||||
mForwardKeyEvents = forwardKeyEvents;
|
||||
}
|
||||
}
|
||||
34
bin/app/java/textinput/State.java
Normal file
34
bin/app/java/textinput/State.java
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
// The state of an editable text region.
|
||||
public final class State {
|
||||
public State(String text_in, int selectionStart_in, int selectionEnd_in,
|
||||
int composingRegionStart_in, int composingRegionEnd_in) {
|
||||
text = text_in;
|
||||
selectionStart = selectionStart_in;
|
||||
selectionEnd = selectionEnd_in;
|
||||
composingRegionStart = composingRegionStart_in;
|
||||
composingRegionEnd = composingRegionEnd_in;
|
||||
}
|
||||
|
||||
public String text;
|
||||
public int selectionStart;
|
||||
public int selectionEnd;
|
||||
public int composingRegionStart;
|
||||
public int composingRegionEnd;
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
main_activity_inject = "java/MainActivity.java"
|
||||
java_files = [
|
||||
"java/ForegroundService.java",
|
||||
"java/videodecode/VideoDecoder.java"
|
||||
"java/videodecode/VideoDecoder.java",
|
||||
"java/textinput/InputConnection.java",
|
||||
"java/textinput/State.java",
|
||||
"java/textinput/Listener.java",
|
||||
"java/textinput/Settings.java",
|
||||
"java/textinput/GameTextInput.java",
|
||||
"java/textinput/Pair.java"
|
||||
]
|
||||
comptime_jar_files = [
|
||||
"android-libs/androidx/core-1.9.0.jar"
|
||||
]
|
||||
runtime_jar_files = [
|
||||
"android-libs/androidx/core-1.9.0.jar"
|
||||
]
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 Dyne.org foundation
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use miniquad::native::android::ndk_sys;
|
||||
use std::ffi::{c_char, c_void, CStr};
|
||||
|
||||
use super::AndroidTextInputState;
|
||||
|
||||
// Opaque type from GameTextInput C API
|
||||
#[repr(C)]
|
||||
pub struct GameTextInput(c_void);
|
||||
|
||||
// Span type used by GameTextInput
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GameTextInputSpan {
|
||||
pub start: i32,
|
||||
pub end: i32,
|
||||
}
|
||||
|
||||
// State structure matching the C header (gametextinput.h:67-85)
|
||||
#[repr(C)]
|
||||
pub struct GameTextInputState {
|
||||
pub text_utf8: *const c_char,
|
||||
pub text_length: i32,
|
||||
pub select: GameTextInputSpan,
|
||||
pub compose: GameTextInputSpan,
|
||||
}
|
||||
|
||||
impl GameTextInputState {
|
||||
pub fn to_owned(&self) -> AndroidTextInputState {
|
||||
let text = unsafe { CStr::from_ptr(self.text_utf8) }.to_str().unwrap().to_string();
|
||||
|
||||
let select = (self.select.start as usize, self.select.end as usize);
|
||||
|
||||
let compose = if self.compose.start >= 0 {
|
||||
assert!(self.compose.end >= 0);
|
||||
Some((self.compose.start as usize, self.compose.end as usize))
|
||||
} else {
|
||||
assert!(self.compose.end < 0);
|
||||
None
|
||||
};
|
||||
|
||||
AndroidTextInputState { text, select, compose }
|
||||
}
|
||||
}
|
||||
|
||||
// Callback type definitions (gametextinput.h:93-94, 221-222)
|
||||
pub type GameTextInputGetStateCallback =
|
||||
unsafe extern "C" fn(*mut c_void, *const GameTextInputState);
|
||||
|
||||
pub type GameTextInputEventCallback = unsafe extern "C" fn(*mut c_void, *const GameTextInputState);
|
||||
|
||||
// FFI bindings to GameTextInput C library
|
||||
extern "C" {
|
||||
// gametextinput.h:111
|
||||
pub fn GameTextInput_init(
|
||||
env: *mut ndk_sys::JNIEnv,
|
||||
max_string_size: u32,
|
||||
) -> *mut GameTextInput;
|
||||
|
||||
// gametextinput.h:140
|
||||
pub fn GameTextInput_destroy(state: *mut GameTextInput);
|
||||
|
||||
// gametextinput.h:235-237
|
||||
pub fn GameTextInput_setEventCallback(
|
||||
state: *mut GameTextInput,
|
||||
callback: Option<GameTextInputEventCallback>,
|
||||
context: *mut c_void,
|
||||
);
|
||||
|
||||
// gametextinput.h:161
|
||||
pub fn GameTextInput_showIme(state: *mut GameTextInput, flags: u32);
|
||||
|
||||
// gametextinput.h:182
|
||||
pub fn GameTextInput_hideIme(state: *mut GameTextInput, flags: u32);
|
||||
|
||||
// gametextinput.h:211-212
|
||||
pub fn GameTextInput_setState(state: *mut GameTextInput, state: *const GameTextInputState);
|
||||
|
||||
// gametextinput.h:200-202
|
||||
pub fn GameTextInput_getState(
|
||||
state: *const GameTextInput,
|
||||
callback: GameTextInputGetStateCallback,
|
||||
context: *mut c_void,
|
||||
);
|
||||
|
||||
// gametextinput.h:121-122
|
||||
pub fn GameTextInput_setInputConnection(
|
||||
state: *mut GameTextInput,
|
||||
input_connection: *mut c_void,
|
||||
);
|
||||
|
||||
// gametextinput.h:132
|
||||
pub fn GameTextInput_processEvent(state: *mut GameTextInput, event_state: *mut c_void);
|
||||
}
|
||||
52
bin/app/src/android/textinput/jni.rs
Normal file
52
bin/app/src/android/textinput/jni.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
/* GameTextInput JNI bridge functions */
|
||||
|
||||
use crate::android::textinput::{init_game_text_input, GAME_TEXT_INPUT};
|
||||
use miniquad::native::android::ndk_sys;
|
||||
|
||||
/// Set the InputConnection for GameTextInput (called from Java)
|
||||
///
|
||||
/// This follows the official Android GameTextInput integration pattern:
|
||||
/// https://developer.android.com/games/agdk/add-support-for-text-input
|
||||
///
|
||||
/// Called from MainActivity when the InputConnection is created. It passes
|
||||
/// the Java InputConnection object to the native GameTextInput library.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment pointer
|
||||
/// * `_class` - JNI class reference (unused)
|
||||
/// * `input_connection` - Java InputConnection object from textinput.InputConnection
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_darkfi_darkfi_1app_MainActivity_setInputConnectionNative(
|
||||
_env: *mut ndk_sys::JNIEnv,
|
||||
_class: ndk_sys::jclass,
|
||||
input_connection: ndk_sys::jobject,
|
||||
) {
|
||||
// Initialize GameTextInput first
|
||||
init_game_text_input();
|
||||
|
||||
// Now set the InputConnection
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.set_input_connection(input_connection);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process IME state event from Java Listener.stateChanged()
|
||||
///
|
||||
/// This follows the official Android GameTextInput integration pattern.
|
||||
/// Called from the Java InputConnection's Listener whenever the IME sends
|
||||
/// a state change (text typed, cursor moved, etc.).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment pointer
|
||||
/// * `_class` - JNI class reference (unused)
|
||||
/// * `soft_keyboard_event` - Java State object from textinput.State
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_darkfi_darkfi_1app_MainActivity_onTextInputEventNative(
|
||||
_env: *mut ndk_sys::JNIEnv,
|
||||
_class: ndk_sys::jclass,
|
||||
soft_keyboard_event: ndk_sys::jobject,
|
||||
) {
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.process_event(soft_keyboard_event);
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,36 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use miniquad::native::android::{self, ndk_sys};
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{c_char, c_void, CString},
|
||||
sync::LazyLock,
|
||||
};
|
||||
use async_channel::Sender as AsyncSender;
|
||||
use miniquad::native::android::attach_jni_env;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
mod ffi;
|
||||
use ffi::{
|
||||
GameTextInput, GameTextInputSpan, GameTextInputState, GameTextInput_destroy,
|
||||
GameTextInput_getState, GameTextInput_hideIme, GameTextInput_init,
|
||||
GameTextInput_setEventCallback, GameTextInput_setState, GameTextInput_showIme,
|
||||
};
|
||||
mod jni;
|
||||
mod state;
|
||||
|
||||
use state::GameTextInput;
|
||||
|
||||
/// Global GameTextInput instance for JNI bridge
|
||||
///
|
||||
/// Single global instance since only ONE editor is active at a time.
|
||||
pub(self) static GAME_TEXT_INPUT: LazyLock<RwLock<Option<GameTextInput>>> =
|
||||
LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
pub(self) fn init_game_text_input() {
|
||||
debug!("AndroidTextInput: Initializing GameTextInput");
|
||||
|
||||
let env = unsafe { attach_jni_env() };
|
||||
let mut gti = GameTextInput::new(env, 0);
|
||||
// Store globally for JNI bridge access
|
||||
*GAME_TEXT_INPUT.write() = Some(gti);
|
||||
|
||||
debug!("AndroidTextInput: GameTextInput initialized");
|
||||
}
|
||||
|
||||
fn is_init() -> bool {
|
||||
GAME_TEXT_INPUT.read().is_some()
|
||||
}
|
||||
|
||||
// Text input state exposed to the rest of the app
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -39,127 +55,54 @@ pub struct AndroidTextInputState {
|
||||
pub compose: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
struct Globals {
|
||||
next_id: usize,
|
||||
senders: HashMap<usize, async_channel::Sender<AndroidTextInputState>>,
|
||||
}
|
||||
|
||||
static GLOBALS: LazyLock<SyncMutex<Globals>> =
|
||||
LazyLock::new(|| SyncMutex::new(Globals { next_id: 0, senders: HashMap::new() }));
|
||||
|
||||
// Callback implementation - sends state update event
|
||||
extern "C" fn game_text_input_callback(ctx: *mut c_void, state: *const GameTextInputState) {
|
||||
// Ensures we can use the void* pointer to store a usize
|
||||
assert_eq!(std::mem::size_of::<usize>(), std::mem::size_of::<*mut c_void>());
|
||||
// ctx is the usize id we passed as void* pointer
|
||||
let id = ctx as usize;
|
||||
let text_state = unsafe { &(*state) }.to_owned();
|
||||
|
||||
let globals = GLOBALS.lock();
|
||||
if let Some(sender) = globals.senders.get(&id) {
|
||||
let _ = sender.try_send(text_state);
|
||||
impl AndroidTextInputState {
|
||||
fn new() -> Self {
|
||||
Self { text: String::new(), select: (0, 0), compose: None }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AndroidTextInput {
|
||||
id: usize,
|
||||
state: *mut GameTextInput,
|
||||
state: AndroidTextInputState,
|
||||
sender: async_channel::Sender<AndroidTextInputState>,
|
||||
is_focus: bool,
|
||||
}
|
||||
|
||||
// SAFETY: GameTextInput is accessed synchronously through show_ime/hide_ime/set_state/get_state
|
||||
// The pointer is valid for the lifetime of the AndroidTextInput instance and is only
|
||||
// accessed from the thread that owns the AndroidTextInput.
|
||||
unsafe impl Send for AndroidTextInput {}
|
||||
unsafe impl Sync for AndroidTextInput {}
|
||||
|
||||
impl AndroidTextInput {
|
||||
pub fn new(sender: async_channel::Sender<AndroidTextInputState>) -> Self {
|
||||
let id = {
|
||||
let mut globals = GLOBALS.lock();
|
||||
let id = globals.next_id;
|
||||
globals.next_id += 1;
|
||||
globals.senders.insert(id, sender);
|
||||
id
|
||||
};
|
||||
|
||||
let state = unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
let state = GameTextInput_init(env, 0);
|
||||
// Ensures we can use the void* pointer to store a usize
|
||||
assert_eq!(std::mem::size_of::<usize>(), std::mem::size_of::<*mut c_void>());
|
||||
GameTextInput_setEventCallback(
|
||||
state,
|
||||
Some(game_text_input_callback),
|
||||
id as *mut c_void,
|
||||
);
|
||||
state
|
||||
};
|
||||
|
||||
Self { id, state }
|
||||
Self { state: AndroidTextInputState::new(), sender, is_focus: false }
|
||||
}
|
||||
|
||||
pub fn show_ime(&self) {
|
||||
unsafe {
|
||||
GameTextInput_showIme(self.state, 0);
|
||||
pub fn show(&mut self) {
|
||||
if !is_init() {
|
||||
return;
|
||||
}
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.event_sender = Some(self.sender.clone());
|
||||
gti.set_state(&self.state);
|
||||
gti.show_ime(0);
|
||||
}
|
||||
self.is_focus = true;
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
if !is_init() {
|
||||
return;
|
||||
}
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.event_sender = None;
|
||||
gti.hide_ime(0);
|
||||
}
|
||||
self.is_focus = false;
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: AndroidTextInputState) {
|
||||
self.state = state;
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.set_state(&self.state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_ime(&self) {
|
||||
unsafe {
|
||||
GameTextInput_hideIme(self.state, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_state(&self, state: &AndroidTextInputState) {
|
||||
let ctext = CString::new(state.text.as_str()).unwrap();
|
||||
|
||||
let select = GameTextInputSpan { start: state.select.0 as i32, end: state.select.1 as i32 };
|
||||
|
||||
let compose = match state.compose {
|
||||
Some((start, end)) => GameTextInputSpan { start: start as i32, end: end as i32 },
|
||||
None => GameTextInputSpan { start: -1, end: -1 },
|
||||
};
|
||||
|
||||
let gt_state = GameTextInputState {
|
||||
text_utf8: ctext.as_ptr(),
|
||||
text_length: state.text.len() as i32,
|
||||
select,
|
||||
compose,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
GameTextInput_setState(self.state, >_state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> AndroidTextInputState {
|
||||
let mut state =
|
||||
AndroidTextInputState { text: String::new(), select: (0, 0), compose: None };
|
||||
|
||||
// This is guaranteed by GameTextInput_getState() to be called sync
|
||||
// so what we are doing is legit here.
|
||||
extern "C" fn callback(ctx: *mut c_void, game_state: *const GameTextInputState) {
|
||||
let state = unsafe { &mut *(ctx as *mut AndroidTextInputState) };
|
||||
*state = unsafe { &(*game_state) }.to_owned();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
GameTextInput_getState(
|
||||
self.state,
|
||||
callback,
|
||||
&mut state as *mut AndroidTextInputState as *mut c_void,
|
||||
);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AndroidTextInput {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
GameTextInput_destroy(self.state);
|
||||
}
|
||||
GLOBALS.lock().senders.remove(&self.id);
|
||||
pub fn get_state(&self) -> &AndroidTextInputState {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
327
bin/app/src/android/textinput/state.rs
Normal file
327
bin/app/src/android/textinput/state.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 Dyne.org foundation
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use async_channel::Sender as AsyncSender;
|
||||
use miniquad::native::android::{ndk_sys, ndk_utils::*};
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use std::ffi::CString;
|
||||
|
||||
use super::AndroidTextInputState;
|
||||
|
||||
const DEFAULT_MAX_STRING_SIZE: usize = 1 << 16;
|
||||
|
||||
pub const SPAN_UNDEFINED: i32 = -1;
|
||||
|
||||
struct StateClassInfo {
|
||||
text: ndk_sys::jfieldID,
|
||||
selection_start: ndk_sys::jfieldID,
|
||||
selection_end: ndk_sys::jfieldID,
|
||||
composing_region_start: ndk_sys::jfieldID,
|
||||
composing_region_end: ndk_sys::jfieldID,
|
||||
}
|
||||
|
||||
pub struct GameTextInput {
|
||||
env: *mut ndk_sys::JNIEnv,
|
||||
state: SyncMutex<AndroidTextInputState>,
|
||||
input_connection: Option<ndk_sys::jobject>,
|
||||
input_connection_class: ndk_sys::jclass,
|
||||
set_soft_keyboard_active_method: ndk_sys::jmethodID,
|
||||
restart_input_method: ndk_sys::jmethodID,
|
||||
state_class_info: StateClassInfo,
|
||||
pub event_sender: Option<AsyncSender<AndroidTextInputState>>
|
||||
}
|
||||
|
||||
impl GameTextInput {
|
||||
pub fn new(env: *mut ndk_sys::JNIEnv, max_string_size: u32) -> Self {
|
||||
unsafe {
|
||||
let find_class = (**env).FindClass.unwrap();
|
||||
|
||||
let state_class_name = b"textinput/State\0";
|
||||
let input_connection_class_name = b"textinput/InputConnection\0";
|
||||
|
||||
let state_java_class = find_class(env, state_class_name.as_ptr() as _);
|
||||
let input_connection_class = find_class(env, input_connection_class_name.as_ptr() as _);
|
||||
|
||||
let input_connection_class =
|
||||
new_global_ref!(env, input_connection_class) as ndk_sys::jclass;
|
||||
|
||||
let get_method_id = (**env).GetMethodID.unwrap();
|
||||
|
||||
let set_state_sig = b"(Ltextinput/State;)V\0";
|
||||
let _input_connection_set_state_method = get_method_id(
|
||||
env,
|
||||
input_connection_class,
|
||||
b"setState\0".as_ptr() as _,
|
||||
set_state_sig.as_ptr() as _,
|
||||
);
|
||||
|
||||
let set_soft_keyboard_active_sig = b"(ZI)V\0";
|
||||
let set_soft_keyboard_active_method = get_method_id(
|
||||
env,
|
||||
input_connection_class,
|
||||
b"setSoftKeyboardActive\0".as_ptr() as _,
|
||||
set_soft_keyboard_active_sig.as_ptr() as _,
|
||||
);
|
||||
|
||||
let restart_input_sig = b"()V\0";
|
||||
let restart_input_method = get_method_id(
|
||||
env,
|
||||
input_connection_class,
|
||||
b"restartInput\0".as_ptr() as _,
|
||||
restart_input_sig.as_ptr() as _,
|
||||
);
|
||||
|
||||
let state_java_class = new_global_ref!(env, state_java_class);
|
||||
|
||||
let get_field_id = (**env).GetFieldID.unwrap();
|
||||
|
||||
let text_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
b"text\0".as_ptr() as _,
|
||||
b"Ljava/lang/String;\0".as_ptr() as _,
|
||||
);
|
||||
let selection_start_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
b"selectionStart\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
let selection_end_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
b"selectionEnd\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
let composing_region_start_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
b"composingRegionStart\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
let composing_region_end_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
b"composingRegionEnd\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
|
||||
let state_class_info = StateClassInfo {
|
||||
text: text_field,
|
||||
selection_start: selection_start_field,
|
||||
selection_end: selection_end_field,
|
||||
composing_region_start: composing_region_start_field,
|
||||
composing_region_end: composing_region_end_field,
|
||||
};
|
||||
|
||||
Self {
|
||||
env,
|
||||
state: SyncMutex::new(AndroidTextInputState::new()),
|
||||
input_connection: None,
|
||||
input_connection_class,
|
||||
set_soft_keyboard_active_method,
|
||||
restart_input_method,
|
||||
state_class_info,
|
||||
event_sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: &AndroidTextInputState) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let jstate = self.state_to_java(state);
|
||||
call_void_method!(
|
||||
self.env,
|
||||
input_connection,
|
||||
"setState",
|
||||
"(Ltextinput/State;)V",
|
||||
jstate
|
||||
);
|
||||
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(self.env, jstate);
|
||||
}
|
||||
}
|
||||
*self.state.lock() = state.clone();
|
||||
}
|
||||
|
||||
fn set_state_inner(&mut self, state: AndroidTextInputState) {
|
||||
*self.state.lock() = state;
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> AndroidTextInputState {
|
||||
self.state.lock().clone()
|
||||
}
|
||||
|
||||
pub fn set_input_connection(&mut self, input_connection: ndk_sys::jobject) {
|
||||
unsafe {
|
||||
if let Some(old_ref) = self.input_connection {
|
||||
let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
|
||||
delete_global_ref(self.env, old_ref);
|
||||
}
|
||||
self.input_connection = Some(new_global_ref!(self.env, input_connection));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_event(&mut self, event_state: ndk_sys::jobject) {
|
||||
let state = self.state_from_java(event_state);
|
||||
if let Some(sender) = &self.event_sender {
|
||||
let _ = sender.try_send(state.clone());
|
||||
}
|
||||
self.set_state_inner(state);
|
||||
}
|
||||
|
||||
pub fn show_ime(&self, flags: u32) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
||||
call_void_method(
|
||||
self.env,
|
||||
input_connection,
|
||||
self.set_soft_keyboard_active_method,
|
||||
1, // active: true
|
||||
flags as ndk_sys::jint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_ime(&self, flags: u32) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
||||
call_void_method(
|
||||
self.env,
|
||||
input_connection,
|
||||
self.set_soft_keyboard_active_method,
|
||||
0, // active: false
|
||||
flags as ndk_sys::jint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restart_input(&self) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
||||
call_void_method(self.env, input_connection, self.restart_input_method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn state_to_java(&self, state: &AndroidTextInputState) -> ndk_sys::jobject {
|
||||
unsafe {
|
||||
let new_string_utf = (**self.env).NewStringUTF.unwrap();
|
||||
let text_str = CString::new(state.text.as_str()).unwrap_or_else(|_| {
|
||||
tracing::error!("Failed to convert text to CString");
|
||||
CString::new("").unwrap()
|
||||
});
|
||||
let jtext = new_string_utf(self.env, text_str.as_ptr());
|
||||
|
||||
let new_object = (**self.env).NewObject.unwrap();
|
||||
let get_method_id = (**self.env).GetMethodID.unwrap();
|
||||
let find_class = (**self.env).FindClass.unwrap();
|
||||
|
||||
let state_class_name = b"textinput/State\0";
|
||||
let state_java_class = find_class(self.env, state_class_name.as_ptr() as _);
|
||||
|
||||
let constructor_sig = b"(Ljava/lang/String;IIII)V\0";
|
||||
let constructor = get_method_id(
|
||||
self.env,
|
||||
state_java_class,
|
||||
b"<init>\0".as_ptr() as _,
|
||||
constructor_sig.as_ptr() as _,
|
||||
);
|
||||
|
||||
let (compose_start, compose_end) = match state.compose {
|
||||
Some((start, end)) => (start as i32, end as i32),
|
||||
None => (SPAN_UNDEFINED, SPAN_UNDEFINED),
|
||||
};
|
||||
|
||||
let jobj = new_object(
|
||||
self.env,
|
||||
state_java_class,
|
||||
constructor,
|
||||
jtext,
|
||||
state.select.0 as i32,
|
||||
state.select.1 as i32,
|
||||
compose_start,
|
||||
compose_end,
|
||||
);
|
||||
|
||||
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(self.env, jtext);
|
||||
delete_local_ref(self.env, state_java_class);
|
||||
jobj
|
||||
}
|
||||
}
|
||||
|
||||
fn state_from_java(&self, event_state: ndk_sys::jobject) -> AndroidTextInputState {
|
||||
unsafe {
|
||||
let get_object_field = (**self.env).GetObjectField.unwrap();
|
||||
let jtext = get_object_field(self.env, event_state, self.state_class_info.text)
|
||||
as ndk_sys::jstring;
|
||||
|
||||
let text = get_utf_str!(self.env, jtext);
|
||||
|
||||
let get_int_field = (**self.env).GetIntField.unwrap();
|
||||
let select_start =
|
||||
get_int_field(self.env, event_state, self.state_class_info.selection_start);
|
||||
let select_end =
|
||||
get_int_field(self.env, event_state, self.state_class_info.selection_end);
|
||||
let compose_start =
|
||||
get_int_field(self.env, event_state, self.state_class_info.composing_region_start);
|
||||
let compose_end =
|
||||
get_int_field(self.env, event_state, self.state_class_info.composing_region_end);
|
||||
|
||||
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(self.env, jtext);
|
||||
|
||||
let compose = if compose_start >= 0 {
|
||||
Some((compose_start as usize, compose_end as usize))
|
||||
} else {
|
||||
assert!(compose_end < 0);
|
||||
None
|
||||
};
|
||||
|
||||
AndroidTextInputState {
|
||||
text,
|
||||
select: (select_start as usize, select_end as usize),
|
||||
compose,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GameTextInput {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
|
||||
if self.input_connection_class != std::ptr::null_mut() {
|
||||
delete_global_ref(self.env, self.input_connection_class);
|
||||
}
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
delete_global_ref(self.env, input_connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for GameTextInput {}
|
||||
unsafe impl Sync for GameTextInput {}
|
||||
@@ -529,23 +529,15 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
|
||||
|
||||
let chatdb_path = get_chatdb_path();
|
||||
let db = sled::open(chatdb_path).expect("cannot open sleddb");
|
||||
for channel in CHANNELS {
|
||||
chat::make(
|
||||
app,
|
||||
content.clone(),
|
||||
channel,
|
||||
&db,
|
||||
i18n_fish,
|
||||
emoji_meshes.clone(),
|
||||
is_first_time,
|
||||
)
|
||||
//for channel in CHANNELS {
|
||||
chat::make(app, content.clone(), "dev", &db, i18n_fish, emoji_meshes.clone(), is_first_time)
|
||||
.await;
|
||||
}
|
||||
menu::make(app, content.clone(), i18n_fish).await;
|
||||
//}
|
||||
//menu::make(app, content.clone(), i18n_fish).await;
|
||||
|
||||
// @@@ Debug stuff @@@
|
||||
//let chatview_node = app.sg_root.lookup_node("/window/content/dev_chat_layer").unwrap();
|
||||
//chatview_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
|
||||
let chatview_node = app.sg_root.lookup_node("/window/content/dev_chat_layer").unwrap();
|
||||
chatview_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
|
||||
//let menu_node = app.sg_root.lookup_node("/window/content/menu_layer").unwrap();
|
||||
//menu_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ impl Editor {
|
||||
lineheight: PropertyFloat32,
|
||||
) -> Self {
|
||||
let (sender, recvr) = async_channel::unbounded();
|
||||
let input = AndroidTextInput::new(sender);
|
||||
|
||||
Self {
|
||||
input: AndroidTextInput::new(sender),
|
||||
input,
|
||||
recvr,
|
||||
|
||||
layout: Default::default(),
|
||||
@@ -70,8 +72,11 @@ impl Editor {
|
||||
|
||||
pub async fn on_text_prop_changed(&mut self) {
|
||||
// Update GameTextInput state
|
||||
let state = AndroidTextInputState { text: self.text.get(), select: (0, 0), compose: None };
|
||||
self.input.set_state(&state);
|
||||
let mut state = self.input.get_state().clone();
|
||||
state.text = self.text.get();
|
||||
state.select = (0, 0);
|
||||
state.compose = None;
|
||||
self.input.set_state(state);
|
||||
// Refresh our layout
|
||||
self.refresh().await;
|
||||
}
|
||||
@@ -84,11 +89,11 @@ impl Editor {
|
||||
self.text.set(atom, &state.text);
|
||||
}
|
||||
|
||||
pub fn focus(&self) {
|
||||
self.input.show_ime();
|
||||
pub fn focus(&mut self) {
|
||||
self.input.show();
|
||||
}
|
||||
pub fn unfocus(&self) {
|
||||
self.input.hide_ime();
|
||||
pub fn unfocus(&mut self) {
|
||||
self.input.hide();
|
||||
}
|
||||
|
||||
pub async fn refresh(&mut self) {
|
||||
@@ -120,16 +125,15 @@ impl Editor {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
pub fn move_to_pos(&self, pos: Point) {
|
||||
pub fn move_to_pos(&mut self, pos: Point) {
|
||||
let cursor = parley::Cursor::from_point(&self.layout, pos.x, pos.y);
|
||||
let cursor_idx = cursor.index();
|
||||
t!(" move_to_pos: {cursor_idx}");
|
||||
let state = AndroidTextInputState {
|
||||
text: self.text.get(),
|
||||
select: (cursor_idx, cursor_idx),
|
||||
compose: None,
|
||||
};
|
||||
self.input.set_state(&state);
|
||||
let mut state = self.input.get_state().clone();
|
||||
state.text = self.text.get();
|
||||
state.select = (cursor_idx, cursor_idx);
|
||||
state.compose = None;
|
||||
self.input.set_state(state);
|
||||
}
|
||||
|
||||
pub async fn select_word_at_point(&mut self, pos: Point) {
|
||||
@@ -161,11 +165,11 @@ impl Editor {
|
||||
pub async fn insert(&mut self, txt: &str, atom: &mut PropertyAtomicGuard) {
|
||||
// TODO: need to verify this is correct
|
||||
// Insert text by updating the state
|
||||
let mut current_state = self.input.get_state();
|
||||
let mut current_state = self.input.get_state().clone();
|
||||
current_state.text.push_str(txt);
|
||||
current_state.select = (current_state.text.len(), current_state.text.len());
|
||||
current_state.compose = None;
|
||||
self.input.set_state(¤t_state);
|
||||
self.input.set_state(current_state);
|
||||
self.on_buffer_changed(atom).await;
|
||||
}
|
||||
|
||||
@@ -213,16 +217,15 @@ impl Editor {
|
||||
parley::Selection::new(anchor, focus)
|
||||
}
|
||||
pub async fn set_selection(&mut self, select_start: usize, select_end: usize) {
|
||||
let state = AndroidTextInputState {
|
||||
text: self.text.get(),
|
||||
select: (select_start, select_end),
|
||||
compose: None,
|
||||
};
|
||||
self.input.set_state(&state);
|
||||
let mut state = self.input.get_state().clone();
|
||||
state.text = self.text.get();
|
||||
state.select = (select_start, select_end);
|
||||
state.compose = None;
|
||||
self.input.set_state(state);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn buffer(&self) -> String {
|
||||
self.input.get_state().text
|
||||
self.input.get_state().text.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,7 +1271,7 @@ impl BaseEdit {
|
||||
panic!("self destroyed before insert_text_method_task was stopped!");
|
||||
};
|
||||
|
||||
let editor = self_.lock_editor().await;
|
||||
let mut editor = self_.lock_editor().await;
|
||||
editor.focus();
|
||||
true
|
||||
}
|
||||
@@ -1290,7 +1290,7 @@ impl BaseEdit {
|
||||
panic!("self destroyed before insert_text_method_task was stopped!");
|
||||
};
|
||||
|
||||
let editor = self_.lock_editor().await;
|
||||
let mut editor = self_.lock_editor().await;
|
||||
editor.unfocus();
|
||||
true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user