Android: support for preedit

(Tested with the typewise keyboard)
This commit is contained in:
Olivier Goffart 2024-02-27 16:23:45 +01:00
parent 1fb162936e
commit c4e0f4a265
2 changed files with 85 additions and 42 deletions

View file

@ -19,14 +19,14 @@ class SlintInputView extends View {
private String mText = ""; private String mText = "";
private int mCursorPosition = 0; private int mCursorPosition = 0;
private int mAnchorPosition = 0; private int mAnchorPosition = 0;
private String mPreedit = ""; private int mPreeditStart = 0;
private int mPreeditOffset; private int mPreeditEnd = 0;
private int mInputType = EditorInfo.TYPE_CLASS_TEXT; private int mInputType = EditorInfo.TYPE_CLASS_TEXT;
private SpannableStringBuilder mEditable; private int mInBatch = 0;
private boolean mPending = false;
private SlintEditable mEditable;
public class SlintEditable extends SpannableStringBuilder { public class SlintEditable extends SpannableStringBuilder {
// private SlintInputView mInputView;
public SlintEditable() { public SlintEditable() {
super(mText); super(mText);
} }
@ -34,18 +34,23 @@ class SlintInputView extends View {
@Override @Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) { public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
super.replace(start, end, tb, tbstart, tbend); super.replace(start, end, tb, tbstart, tbend);
mText = toString(); System.out.println("replace '" + toString() + "' mInBatch=" + mInBatch);
mCursorPosition = Selection.getSelectionStart(this); if (mInBatch == 0) {
mAnchorPosition = Selection.getSelectionEnd(this); update();
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, "", mCursorPosition); } else {
mPending = true;
}
return this; return this;
} }
@Override public void update() {
public void setSpan(Object what, int start, int end, int flags) { mPending = false;
super.setSpan(what, start, end, flags); mText = toString();
mCursorPosition = Selection.getSelectionStart(this); mCursorPosition = Selection.getSelectionStart(this);
mAnchorPosition = Selection.getSelectionEnd(this); mAnchorPosition = Selection.getSelectionEnd(this);
mPreeditStart = BaseInputConnection.getComposingSpanStart(this);
mPreeditEnd = BaseInputConnection.getComposingSpanEnd(this);
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreeditStart, mPreeditEnd);
} }
} }
@ -72,18 +77,33 @@ class SlintInputView extends View {
public Editable getEditable() { public Editable getEditable() {
return mEditable; return mEditable;
} }
@Override
public boolean beginBatchEdit() {
mInBatch += 1;
return super.beginBatchEdit();
}
@Override
public boolean endBatchEdit() {
mInBatch -= 1;
if (mInBatch == 0 && mPending) {
mEditable.update();
}
return super.endBatchEdit();
}
}; };
} }
public void setText(String text, int cursorPosition, int anchorPosition, String preedit, int preeditOffset, public void setText(String text, int cursorPosition, int anchorPosition, int preeditStart, int preeditEnd,
int inputType) { int inputType) {
boolean restart = mInputType != inputType || !mText.equals(text) || mCursorPosition != cursorPosition boolean restart = mInputType != inputType || !mText.equals(text) || mCursorPosition != cursorPosition
|| mAnchorPosition != anchorPosition; || mAnchorPosition != anchorPosition;
mText = text; mText = text;
mCursorPosition = cursorPosition; mCursorPosition = cursorPosition;
mAnchorPosition = anchorPosition; mAnchorPosition = anchorPosition;
mPreedit = preedit; mPreeditStart = preeditStart;
mPreeditOffset = preeditOffset; mPreeditEnd = preeditEnd;
mInputType = inputType; mInputType = inputType;
if (restart) { if (restart) {
@ -148,12 +168,12 @@ public class SlintAndroidJavaHelper {
}); });
} }
static public native void updateText(String text, int cursorPosition, int anchorPosition, String preedit, static public native void updateText(String text, int cursorPosition, int anchorPosition, int preeditStart,
int preeditOffset); int preeditOffset);
static public native void setDarkMode(boolean dark); static public native void setDarkMode(boolean dark);
public void set_imm_data(String text, int cursor_position, int anchor_position, String preedit, int preedit_offset, public void set_imm_data(String text, int cursor_position, int anchor_position, int preedit_start, int preedit_end,
int rect_x, int rect_y, int rect_w, int rect_h, int input_type) { int rect_x, int rect_y, int rect_w, int rect_h, int input_type) {
mActivity.runOnUiThread(new Runnable() { mActivity.runOnUiThread(new Runnable() {
@ -164,7 +184,7 @@ public class SlintAndroidJavaHelper {
mInputView.setLayoutParams(layoutParams); mInputView.setLayoutParams(layoutParams);
int selStart = Math.min(cursor_position, anchor_position); int selStart = Math.min(cursor_position, anchor_position);
int selEnd = Math.max(cursor_position, anchor_position); int selEnd = Math.max(cursor_position, anchor_position);
mInputView.setText(text, selStart, selEnd, preedit, preedit_offset, input_type); mInputView.setText(text, selStart, selEnd, preedit_start, preedit_end, input_type);
} }
}); });
} }

View file

@ -52,7 +52,7 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
let methods = [ let methods = [
jni::NativeMethod { jni::NativeMethod {
name: "updateText".into(), name: "updateText".into(),
sig: "(Ljava/lang/String;IILjava/lang/String;I)V".into(), sig: "(Ljava/lang/String;IIII)V".into(),
fn_ptr: Java_SlintAndroidJavaHelper_updateText as *mut _, fn_ptr: Java_SlintAndroidJavaHelper_updateText as *mut _,
}, },
jni::NativeMethod { jni::NativeMethod {
@ -105,9 +105,22 @@ impl JavaHelper {
data: &i_slint_core::window::InputMethodProperties, data: &i_slint_core::window::InputMethodProperties,
) -> Result<(), jni::errors::Error> { ) -> Result<(), jni::errors::Error> {
self.with_jni_env(|env, helper| { self.with_jni_env(|env, helper| {
let text = &env.new_string(data.text.as_str())?; let mut text = data.text.to_string();
let preedit_text = env.new_string(data.preedit_text.as_str())?; let mut cursor_position = data.cursor_position;
let to_utf16 = |x| convert_utf8_index_to_utf16(&data.text, x as usize); let mut anchor_position = data.anchor_position.unwrap_or(data.cursor_position);
if !data.preedit_text.is_empty() {
text.insert_str(data.preedit_offset, data.preedit_text.as_str());
if cursor_position >= data.preedit_offset {
cursor_position += data.preedit_text.len()
}
if anchor_position >= data.preedit_offset {
anchor_position += data.preedit_text.len()
}
}
let to_utf16 = |x| convert_utf8_index_to_utf16(&text, x as usize);
let text = &env.new_string(text.as_str())?;
let class_it = env.find_class("android/text/InputType")?; let class_it = env.find_class("android/text/InputType")?;
let input_type = match data.input_type { let input_type = match data.input_type {
@ -128,15 +141,13 @@ impl JavaHelper {
env.call_method( env.call_method(
helper, helper,
"set_imm_data", "set_imm_data",
"(Ljava/lang/String;IILjava/lang/String;IIIIII)V", "(Ljava/lang/String;IIIIIIIII)V",
&[ &[
JValue::Object(&text), JValue::Object(&text),
JValue::from(to_utf16(data.cursor_position) as jint), JValue::from(to_utf16(cursor_position) as jint),
JValue::from( JValue::from(to_utf16(anchor_position) as jint),
to_utf16(data.anchor_position.unwrap_or(data.cursor_position)) as jint
),
JValue::Object(&preedit_text),
JValue::from(to_utf16(data.preedit_offset) as jint), JValue::from(to_utf16(data.preedit_offset) as jint),
JValue::from(to_utf16(data.preedit_offset + data.preedit_text.len()) as jint),
JValue::from(data.cursor_rect_origin.x as jint), JValue::from(data.cursor_rect_origin.x as jint),
JValue::from(data.cursor_rect_origin.y as jint), JValue::from(data.cursor_rect_origin.y as jint),
JValue::from(data.cursor_rect_size.width as jint), JValue::from(data.cursor_rect_size.width as jint),
@ -175,8 +186,8 @@ extern "system" fn Java_SlintAndroidJavaHelper_updateText(
text: JString, text: JString,
cursor_position: jint, cursor_position: jint,
anchor_position: jint, anchor_position: jint,
preedit: JString, preedit_start: jint,
preedit_offset: jint, preedit_end: jint,
) { ) {
fn make_shared_string(env: &mut JNIEnv, string: &JString) -> Option<SharedString> { fn make_shared_string(env: &mut JNIEnv, string: &JString) -> Option<SharedString> {
let java_str = env.get_string(&string).ok()?; let java_str = env.get_string(&string).ok()?;
@ -184,24 +195,36 @@ extern "system" fn Java_SlintAndroidJavaHelper_updateText(
Some(SharedString::from(decoded.as_ref())) Some(SharedString::from(decoded.as_ref()))
} }
let Some(text) = make_shared_string(&mut env, &text) else { return }; let Some(text) = make_shared_string(&mut env, &text) else { return };
let Some(preedit) = make_shared_string(&mut env, &preedit) else { return };
let cursor_position = convert_utf16_index_to_utf8(&text, cursor_position as usize); let cursor_position = convert_utf16_index_to_utf8(&text, cursor_position as usize);
let anchor_position = convert_utf16_index_to_utf8(&text, anchor_position as usize); let anchor_position = convert_utf16_index_to_utf8(&text, anchor_position as usize);
let preedit_offset = convert_utf16_index_to_utf8(&text, preedit_offset as usize) as i32; let preedit_start = convert_utf16_index_to_utf8(&text, preedit_start as usize);
let preedit_end = convert_utf16_index_to_utf8(&text, preedit_end as usize);
i_slint_core::api::invoke_from_event_loop(move || { i_slint_core::api::invoke_from_event_loop(move || {
if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) { if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
let runtime_window = i_slint_core::window::WindowInner::from_pub(&adaptor.window); let runtime_window = i_slint_core::window::WindowInner::from_pub(&adaptor.window);
let event = i_slint_core::input::KeyEvent { let event = if preedit_start != preedit_end {
event_type: i_slint_core::input::KeyEventType::UpdateComposition, let adjust = |pos| if pos <= preedit_start { pos } else if pos >= preedit_end { pos - preedit_end + preedit_start } else { preedit_start } as i32;
text, i_slint_core::input::KeyEvent {
replacement_range: Some(i32::MIN..i32::MAX), event_type: i_slint_core::input::KeyEventType::UpdateComposition,
cursor_position: Some(cursor_position as _), text: i_slint_core::format!( "{}{}", &text[..preedit_start], &text[preedit_end..]),
anchor_position: Some(anchor_position as _), preedit_text: text[preedit_start..preedit_end].into(),
preedit_selection: (!preedit.is_empty()) preedit_selection: Some(0..(preedit_end - preedit_start) as i32),
.then(|| preedit_offset..(preedit_offset + preedit.len() as i32)), replacement_range: Some(i32::MIN..i32::MAX),
preedit_text: preedit, cursor_position: Some(adjust(cursor_position)),
..Default::default() anchor_position: Some(adjust(anchor_position)),
..Default::default()
}
} else {
i_slint_core::input::KeyEvent {
event_type: i_slint_core::input::KeyEventType::CommitComposition,
text,
replacement_range: Some(i32::MIN..i32::MAX),
cursor_position: Some(cursor_position as _),
anchor_position: Some(anchor_position as _),
..Default::default()
}
}; };
runtime_window.process_key_input(event); runtime_window.process_key_input(event);
} }