Android: simplify the text input with SpannableStringBuilder

Also needed some change on the Text item to avoid sending ime callback
for intermediate states
This commit is contained in:
Olivier Goffart 2024-02-27 14:14:30 +01:00
parent 2b08de093a
commit 1fb162936e
2 changed files with 171 additions and 88 deletions

View file

@ -7,11 +7,13 @@ import android.view.inputmethod.InputConnection;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Rect; import android.graphics.Rect;
import android.text.Editable;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.app.Activity; import android.app.Activity;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.ExtractedText;
class SlintInputView extends View { class SlintInputView extends View {
private String mText = ""; private String mText = "";
@ -20,11 +22,38 @@ class SlintInputView extends View {
private String mPreedit = ""; private String mPreedit = "";
private int mPreeditOffset; private int mPreeditOffset;
private int mInputType = EditorInfo.TYPE_CLASS_TEXT; private int mInputType = EditorInfo.TYPE_CLASS_TEXT;
private SpannableStringBuilder mEditable;
public class SlintEditable extends SpannableStringBuilder {
// private SlintInputView mInputView;
public SlintEditable() {
super(mText);
}
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
super.replace(start, end, tb, tbstart, tbend);
mText = toString();
mCursorPosition = Selection.getSelectionStart(this);
mAnchorPosition = Selection.getSelectionEnd(this);
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, "", mCursorPosition);
return this;
}
@Override
public void setSpan(Object what, int start, int end, int flags) {
super.setSpan(what, start, end, flags);
mCursorPosition = Selection.getSelectionStart(this);
mAnchorPosition = Selection.getSelectionEnd(this);
}
}
public SlintInputView(Context context) { public SlintInputView(Context context) {
super(context); super(context);
setFocusable(true); setFocusable(true);
setFocusableInTouchMode(true); setFocusableInTouchMode(true);
mEditable = new SlintEditable();
} }
@Override @Override
@ -38,73 +67,18 @@ class SlintInputView extends View {
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
outAttrs.initialSelStart = mCursorPosition; outAttrs.initialSelStart = mCursorPosition;
outAttrs.initialSelEnd = mAnchorPosition; outAttrs.initialSelEnd = mAnchorPosition;
return new BaseInputConnection(this, false) { return new BaseInputConnection(this, true) {
@Override @Override
public CharSequence getTextBeforeCursor(int n, int flags) { public Editable getEditable() {
return mText.substring(0, mCursorPosition); return mEditable;
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return mText.substring(mCursorPosition);
}
@Override
public CharSequence getSelectedText(int flags) {
if (mCursorPosition != mAnchorPosition) {
return mText.substring(mCursorPosition, mAnchorPosition);
}
return null;
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
mText = new StringBuilder(mText).delete(mCursorPosition, mAnchorPosition).insert(mCursorPosition, text)
.toString();
mPreedit = "";
if (newCursorPosition > 0) {
mCursorPosition = mCursorPosition + text.length() + newCursorPosition - 1;
} else {
mCursorPosition = mCursorPosition + newCursorPosition;
}
mAnchorPosition = mCursorPosition;
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreedit, mPreeditOffset);
// return super.commitText(text, newCursorPosition);
return true;
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
mText = new StringBuilder(mText).delete(mCursorPosition - beforeLength, mAnchorPosition + afterLength)
.toString();
mCursorPosition = mCursorPosition - beforeLength;
mAnchorPosition = mCursorPosition;
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreedit, mPreeditOffset);
return true;
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
mPreedit = text.toString();
mPreeditOffset = newCursorPosition;
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreedit, mPreeditOffset);
return super.setComposingText(text, newCursorPosition);
}
@Override
public boolean setSelection(int start, int end) {
mCursorPosition = start;
mAnchorPosition = end;
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreedit, mPreeditOffset);
return true;
} }
}; };
} }
public void setText(String text, int cursorPosition, int anchorPosition, String preedit, int preeditOffset, public void setText(String text, int cursorPosition, int anchorPosition, String preedit, int preeditOffset,
int inputType) { int inputType) {
boolean restart = mInputType != inputType || !mText.equals(text); boolean restart = mInputType != inputType || !mText.equals(text) || mCursorPosition != cursorPosition
boolean update_selection = mCursorPosition != cursorPosition || mAnchorPosition != anchorPosition; || mAnchorPosition != anchorPosition;
mText = text; mText = text;
mCursorPosition = cursorPosition; mCursorPosition = cursorPosition;
mAnchorPosition = anchorPosition; mAnchorPosition = anchorPosition;
@ -112,17 +86,12 @@ class SlintInputView extends View {
mPreeditOffset = preeditOffset; mPreeditOffset = preeditOffset;
mInputType = inputType; mInputType = inputType;
InputMethodManager imm = (InputMethodManager) this.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (restart) { if (restart) {
mEditable = new SlintEditable();
Selection.setSelection(mEditable, cursorPosition, anchorPosition);
InputMethodManager imm = (InputMethodManager) this.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.restartInput(this); imm.restartInput(this);
} else if (update_selection) {
ExtractedText extractedText = new ExtractedText();
extractedText.text = mText;
extractedText.startOffset = mPreeditOffset;
extractedText.selectionStart = mCursorPosition;
extractedText.selectionEnd = mAnchorPosition;
imm.updateExtractedText(this, 0, extractedText);
imm.updateSelection(this, cursorPosition, anchorPosition, cursorPosition, anchorPosition);
} }
} }

View file

@ -367,7 +367,13 @@ impl Item for TextInput {
self.ensure_focus_and_ime(window_adapter, self_rc); self.ensure_focus_and_ime(window_adapter, self_rc);
match click_count % 3 { match click_count % 3 {
0 => self.set_cursor_position(clicked_offset, true, window_adapter, self_rc), 0 => self.set_cursor_position(
clicked_offset,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
),
1 => self.select_word(window_adapter, self_rc), 1 => self.select_word(window_adapter, self_rc),
2 => self.select_paragraph(window_adapter, self_rc), 2 => self.select_paragraph(window_adapter, self_rc),
_ => unreachable!(), _ => unreachable!(),
@ -388,7 +394,14 @@ impl Item for TextInput {
MouseEvent::Released { position, button: PointerEventButton::Middle, .. } => { MouseEvent::Released { position, button: PointerEventButton::Middle, .. } => {
let clicked_offset = self.byte_offset_for_position(position, window_adapter) as i32; let clicked_offset = self.byte_offset_for_position(position, window_adapter) as i32;
self.as_ref().anchor_position_byte_offset.set(clicked_offset); self.as_ref().anchor_position_byte_offset.set(clicked_offset);
self.set_cursor_position(clicked_offset, true, window_adapter, self_rc); self.set_cursor_position(
clicked_offset,
true,
// We trigger the callbacks because paste_clipboard might not if there is no clipboard
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
self.paste_clipboard(window_adapter, self_rc, Clipboard::SelectionClipboard); self.paste_clipboard(window_adapter, self_rc, Clipboard::SelectionClipboard);
} }
MouseEvent::Exit => { MouseEvent::Exit => {
@ -405,8 +418,17 @@ impl Item for TextInput {
if pressed > 0 { if pressed > 0 {
let clicked_offset = let clicked_offset =
self.byte_offset_for_position(position, window_adapter) as i32; self.byte_offset_for_position(position, window_adapter) as i32;
self.set_cursor_position(
self.set_cursor_position(clicked_offset, true, window_adapter, self_rc); clicked_offset,
true,
if (pressed - 1) % 3 == 0 {
TextChangeNotify::TriggerCallbacks
} else {
TextChangeNotify::SkipCallbacks
},
window_adapter,
self_rc,
);
match (pressed - 1) % 3 { match (pressed - 1) % 3 {
0 => (), 0 => (),
1 => self.select_word(window_adapter, self_rc), 1 => self.select_word(window_adapter, self_rc),
@ -439,6 +461,7 @@ impl Item for TextInput {
self, self,
direction, direction,
event.modifiers.into(), event.modifiers.into(),
TextChangeNotify::TriggerCallbacks,
window_adapter, window_adapter,
self_rc, self_rc,
); );
@ -581,7 +604,13 @@ impl Item for TextInput {
self.as_ref().text.set(text.into()); self.as_ref().text.set(text.into());
let new_cursor_pos = (insert_pos + event.text.len()) as i32; let new_cursor_pos = (insert_pos + event.text.len()) as i32;
self.as_ref().anchor_position_byte_offset.set(new_cursor_pos); self.as_ref().anchor_position_byte_offset.set(new_cursor_pos);
self.set_cursor_position(new_cursor_pos, true, window_adapter, self_rc); self.set_cursor_position(
new_cursor_pos,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
// Keep the cursor visible when inserting text. Blinking should only occur when // Keep the cursor visible when inserting text. Blinking should only occur when
// nothing is entered or the cursor isn't moved. // nothing is entered or the cursor isn't moved.
@ -600,11 +629,29 @@ impl Item for TextInput {
// Set the selection so the call to insert erases it // Set the selection so the call to insert erases it
self.anchor_position_byte_offset.set(cursor.saturating_add(r.start)); self.anchor_position_byte_offset.set(cursor.saturating_add(r.start));
self.cursor_position_byte_offset.set(cursor.saturating_add(r.end)); self.cursor_position_byte_offset.set(cursor.saturating_add(r.end));
if event.text.is_empty() {
self.delete_selection(
window_adapter,
self_rc,
if event.cursor_position.is_none() {
TextChangeNotify::TriggerCallbacks
} else {
// will be updated by the set_cursor_position later
TextChangeNotify::SkipCallbacks
},
);
}
} }
self.insert(&event.text, window_adapter, self_rc); self.insert(&event.text, window_adapter, self_rc);
if let Some(cursor) = event.cursor_position { if let Some(cursor) = event.cursor_position {
self.anchor_position_byte_offset.set(event.anchor_position.unwrap_or(cursor)); self.anchor_position_byte_offset.set(event.anchor_position.unwrap_or(cursor));
self.set_cursor_position(cursor, true, window_adapter, self_rc); self.set_cursor_position(
cursor,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
KeyEventResult::EventAccepted KeyEventResult::EventAccepted
} }
@ -842,6 +889,7 @@ impl TextInput {
self: Pin<&Self>, self: Pin<&Self>,
direction: TextCursorDirection, direction: TextCursorDirection,
anchor_mode: AnchorMode, anchor_mode: AnchorMode,
trigger_callbacks: TextChangeNotify,
window_adapter: &Rc<dyn WindowAdapter>, window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc, self_rc: &ItemRc,
) -> bool { ) -> bool {
@ -941,6 +989,7 @@ impl TextInput {
self.set_cursor_position( self.set_cursor_position(
new_cursor_pos as i32, new_cursor_pos as i32,
reset_preferred_x_pos, reset_preferred_x_pos,
trigger_callbacks,
window_adapter, window_adapter,
self_rc, self_rc,
); );
@ -956,6 +1005,7 @@ impl TextInput {
self: Pin<&Self>, self: Pin<&Self>,
new_position: i32, new_position: i32,
reset_preferred_x_pos: bool, reset_preferred_x_pos: bool,
trigger_callbacks: TextChangeNotify,
window_adapter: &Rc<dyn WindowAdapter>, window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc, self_rc: &ItemRc,
) { ) {
@ -968,8 +1018,10 @@ impl TextInput {
if reset_preferred_x_pos { if reset_preferred_x_pos {
self.preferred_x_pos.set(pos.x); self.preferred_x_pos.set(pos.x);
} }
Self::FIELD_OFFSETS.cursor_position_changed.apply_pin(self).call(&(pos,)); if trigger_callbacks == TextChangeNotify::TriggerCallbacks {
self.update_ime(window_adapter, self_rc); Self::FIELD_OFFSETS.cursor_position_changed.apply_pin(self).call(&(pos,));
self.update_ime(window_adapter, self_rc);
}
} }
} }
@ -991,7 +1043,13 @@ impl TextInput {
self_rc: &ItemRc, self_rc: &ItemRc,
) { ) {
if !self.has_selection() { if !self.has_selection() {
self.move_cursor(step, AnchorMode::KeepAnchor, window_adapter, self_rc); self.move_cursor(
step,
AnchorMode::KeepAnchor,
TextChangeNotify::SkipCallbacks,
window_adapter,
self_rc,
);
} }
self.delete_selection(window_adapter, self_rc, TextChangeNotify::TriggerCallbacks); self.delete_selection(window_adapter, self_rc, TextChangeNotify::TriggerCallbacks);
} }
@ -1032,7 +1090,13 @@ impl TextInput {
}); });
if trigger_callbacks == TextChangeNotify::TriggerCallbacks { if trigger_callbacks == TextChangeNotify::TriggerCallbacks {
self.set_cursor_position(anchor as i32, true, window_adapter, self_rc); self.set_cursor_position(
anchor as i32,
true,
trigger_callbacks,
window_adapter,
self_rc,
);
Self::FIELD_OFFSETS.edited.apply_pin(self).call(&()); Self::FIELD_OFFSETS.edited.apply_pin(self).call(&());
} else { } else {
self.cursor_position_byte_offset.set(anchor as i32); self.cursor_position_byte_offset.set(anchor as i32);
@ -1129,7 +1193,13 @@ impl TextInput {
let cursor_pos = cursor_pos + text_to_insert.len(); let cursor_pos = cursor_pos + text_to_insert.len();
self.text.set(text.into()); self.text.set(text.into());
self.anchor_position_byte_offset.set(cursor_pos as i32); self.anchor_position_byte_offset.set(cursor_pos as i32);
self.set_cursor_position(cursor_pos as i32, true, window_adapter, self_rc); self.set_cursor_position(
cursor_pos as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
Self::FIELD_OFFSETS.edited.apply_pin(self).call(&()); Self::FIELD_OFFSETS.edited.apply_pin(self).call(&());
} }
@ -1150,19 +1220,27 @@ impl TextInput {
let safe_end = safe_byte_offset(end, &text); let safe_end = safe_byte_offset(end, &text);
self.as_ref().anchor_position_byte_offset.set(safe_start as i32); self.as_ref().anchor_position_byte_offset.set(safe_start as i32);
self.set_cursor_position(safe_end as i32, true, window_adapter, self_rc); self.set_cursor_position(
safe_end as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
pub fn select_all(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) { pub fn select_all(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
self.move_cursor( self.move_cursor(
TextCursorDirection::StartOfText, TextCursorDirection::StartOfText,
AnchorMode::MoveAnchor, AnchorMode::MoveAnchor,
TextChangeNotify::SkipCallbacks,
window_adapter, window_adapter,
self_rc, self_rc,
); );
self.move_cursor( self.move_cursor(
TextCursorDirection::EndOfText, TextCursorDirection::EndOfText,
AnchorMode::KeepAnchor, AnchorMode::KeepAnchor,
TextChangeNotify::TriggerCallbacks,
window_adapter, window_adapter,
self_rc, self_rc,
); );
@ -1182,7 +1260,13 @@ impl TextInput {
(next_word_boundary(&text, anchor), prev_word_boundary(&text, cursor)) (next_word_boundary(&text, anchor), prev_word_boundary(&text, cursor))
}; };
self.as_ref().anchor_position_byte_offset.set(new_a as i32); self.as_ref().anchor_position_byte_offset.set(new_a as i32);
self.set_cursor_position(new_c as i32, true, window_adapter, self_rc); self.set_cursor_position(
new_c as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
fn select_paragraph( fn select_paragraph(
@ -1199,7 +1283,13 @@ impl TextInput {
(next_paragraph_boundary(&text, anchor), prev_paragraph_boundary(&text, cursor)) (next_paragraph_boundary(&text, anchor), prev_paragraph_boundary(&text, cursor))
}; };
self.as_ref().anchor_position_byte_offset.set(new_a as i32); self.as_ref().anchor_position_byte_offset.set(new_a as i32);
self.set_cursor_position(new_c as i32, true, window_adapter, self_rc); self.set_cursor_position(
new_c as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
pub fn copy(self: Pin<&Self>, w: &Rc<dyn WindowAdapter>, _: &ItemRc) { pub fn copy(self: Pin<&Self>, w: &Rc<dyn WindowAdapter>, _: &ItemRc) {
@ -1417,7 +1507,13 @@ impl TextInput {
self.text.set(text.into()); self.text.set(text.into());
self.anchor_position_byte_offset.set(last.anchor as i32); self.anchor_position_byte_offset.set(last.anchor as i32);
self.set_cursor_position(last.cursor as i32, true, window_adapter, self_rc); self.set_cursor_position(
last.cursor as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
UndoItemKind::TextRemove => { UndoItemKind::TextRemove => {
let mut text: String = self.text().into(); let mut text: String = self.text().into();
@ -1425,7 +1521,13 @@ impl TextInput {
self.text.set(text.into()); self.text.set(text.into());
self.anchor_position_byte_offset.set(last.anchor as i32); self.anchor_position_byte_offset.set(last.anchor as i32);
self.set_cursor_position(last.cursor as i32, true, window_adapter, self_rc); self.set_cursor_position(
last.cursor as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
} }
self.undo_items.set(items); self.undo_items.set(items);
@ -1448,7 +1550,13 @@ impl TextInput {
self.text.set(text.into()); self.text.set(text.into());
self.anchor_position_byte_offset.set(last.anchor as i32); self.anchor_position_byte_offset.set(last.anchor as i32);
self.set_cursor_position(last.cursor as i32, true, window_adapter, self_rc); self.set_cursor_position(
last.cursor as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
UndoItemKind::TextRemove => { UndoItemKind::TextRemove => {
let text: String = self.text().into(); let text: String = self.text().into();
@ -1457,7 +1565,13 @@ impl TextInput {
self.text.set(text.into()); self.text.set(text.into());
self.anchor_position_byte_offset.set(last.anchor as i32); self.anchor_position_byte_offset.set(last.anchor as i32);
self.set_cursor_position(last.cursor as i32, true, window_adapter, self_rc); self.set_cursor_position(
last.cursor as i32,
true,
TextChangeNotify::TriggerCallbacks,
window_adapter,
self_rc,
);
} }
} }