From 324756535e139aacfb9d828a5bc9a2a6fef634ea Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Sun, 24 Jul 2022 20:02:48 +0200 Subject: [PATCH] Automatic capitalisation at beginning of sentences Keep track of end-of-sentence characters while typing and automatically enable shift when appropriate. The last few characters just before the cursor need to be queried in some cases: Begin of input, cursor has moved or text is deleted. This might have a performance cost. This normally only enable shift but it also needs to disable shift when the cursor moves. --- srcs/juloo.keyboard2/Autocapitalisation.java | 119 +++++++++++++++++++ srcs/juloo.keyboard2/Keyboard2.java | 27 ++++- srcs/juloo.keyboard2/Keyboard2View.java | 21 ++++ srcs/juloo.keyboard2/KeyboardData.java | 30 +++++ srcs/juloo.keyboard2/Pointers.java | 25 +++- 5 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 srcs/juloo.keyboard2/Autocapitalisation.java diff --git a/srcs/juloo.keyboard2/Autocapitalisation.java b/srcs/juloo.keyboard2/Autocapitalisation.java new file mode 100644 index 0000000..1affdab --- /dev/null +++ b/srcs/juloo.keyboard2/Autocapitalisation.java @@ -0,0 +1,119 @@ +package juloo.keyboard2; + +import android.text.InputType; +import android.text.TextUtils; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.KeyEvent; + +final class Autocapitalisation +{ + private boolean _enabled = false; + private boolean _beginning_of_sentence = false; + + /** Keep track of the cursor to differentiate 'selection_updated' events + corresponding to typing from cursor movement. */ + private int _cursor = 0; + + public boolean should_enable_shift() + { + return _enabled && _beginning_of_sentence; + } + + /** Returns [true] if shift should be on initially. The input connection + isn't stored. */ + public void started(EditorInfo info, InputConnection ic) + { + if ((info.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) == 0) + { + _enabled = false; + return; + } + _enabled = true; + _beginning_of_sentence = ((info.initialCapsMode & TextUtils.CAP_MODE_SENTENCES) != 0); + _cursor = 0; // Just a guess + scan_text_before_cursor(10, ic); + } + + public void typed(CharSequence c) + { + for (int i = 0; i < c.length(); i++) + typed(c.charAt(i)); + } + + public void typed(char c) + { + _cursor++; + if (is_beginning_of_sentence(c)) + _beginning_of_sentence = true; + else if (!ignore_at_beginning_of_sentence(c)) + _beginning_of_sentence = false; + } + + public void selection_updated(int old_cursor, int new_cursor, InputConnection ic) + { + if (new_cursor == _cursor) + return; + // Text has been inserted + if (old_cursor == _cursor && new_cursor > old_cursor) + { + scan_text_before_cursor(Math.min(new_cursor - old_cursor, 10), ic); + } + else + { + // Cursor has moved or [_cursor] wasn't uptodate + _beginning_of_sentence = false; + scan_text_before_cursor(10, ic); + } + _cursor = new_cursor; + } + + /** Updates [_cursor]. */ + private void scan_text_before_cursor(int range, InputConnection ic) + { + if (!_enabled) // Don't query characters if disabled + return; + CharSequence text_before = ic.getTextBeforeCursor(range, 0); + if (text_before == null) + { + _beginning_of_sentence = false; + } + else + { + _beginning_of_sentence = true; + typed(text_before); + } + } + + private boolean ignore_at_beginning_of_sentence(char c) + { + switch (c) + { + case ' ': + case '"': + case '\'': + case '(': + case '«': + return true; + default: + return false; + } + } + + private boolean is_beginning_of_sentence(char c) + { + switch (c) + { + case '.': + case ';': + case '\n': + case '!': + case '?': + case '¿': + case '¡': + return true; + default: + return false; + } + } +} diff --git a/srcs/juloo.keyboard2/Keyboard2.java b/srcs/juloo.keyboard2/Keyboard2.java index 8483a50..cec2b44 100644 --- a/srcs/juloo.keyboard2/Keyboard2.java +++ b/srcs/juloo.keyboard2/Keyboard2.java @@ -35,6 +35,7 @@ public class Keyboard2 extends InputMethodService private ViewGroup _emojiPane = null; private Config _config; + private Autocapitalisation _autocap = new Autocapitalisation(); private boolean _debug_logs = false; @@ -57,6 +58,14 @@ public class Keyboard2 extends InputMethodService _debug_logs = getResources().getBoolean(R.bool.debug_logs); } + private void update_shift_state(boolean might_disable) + { + if (_autocap.should_enable_shift()) + _keyboardView.set_shift_state(true); + else if (might_disable) + _keyboardView.set_shift_state(false); + } + private List getEnabledSubtypes(InputMethodManager imm) { String pkg = getPackageName(); @@ -163,7 +172,7 @@ public class Keyboard2 extends InputMethodService return getResources().getString(res); } - private void refreshEditorInfo(EditorInfo info) + private void refresh_action_label(EditorInfo info) { // First try to look at 'info.actionLabel', if it isn't set, look at // 'imeOptions'. @@ -210,11 +219,13 @@ public class Keyboard2 extends InputMethodService public void onStartInputView(EditorInfo info, boolean restarting) { refreshConfig(); - refreshEditorInfo(info); + refresh_action_label(info); if ((info.inputType & InputType.TYPE_CLASS_NUMBER) != 0) _keyboardView.setKeyboard(getLayout(R.xml.numeric)); else _keyboardView.setKeyboard(getLayout(_currentTextLayout)); + _autocap.started(info, getCurrentInputConnection()); + update_shift_state(false); setInputView(_keyboardView); if (_debug_logs) log_editor_info(info); @@ -236,6 +247,14 @@ public class Keyboard2 extends InputMethodService _keyboardView.setKeyboard(getLayout(_currentTextLayout)); } + @Override + public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) + { + super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); + _autocap.selection_updated(oldSelStart, newSelStart, getCurrentInputConnection()); + update_shift_state(true); + } + @Override public void onFinishInputView(boolean finishingInput) { @@ -330,11 +349,15 @@ public class Keyboard2 extends InputMethodService public void commitText(String text) { getCurrentInputConnection().commitText(text, 1); + _autocap.typed(text); + update_shift_state(false); } public void commitChar(char c) { sendKeyChar(c); + _autocap.typed(c); + update_shift_state(false); } } diff --git a/srcs/juloo.keyboard2/Keyboard2View.java b/srcs/juloo.keyboard2/Keyboard2View.java index fed2be6..d946aa1 100644 --- a/srcs/juloo.keyboard2/Keyboard2View.java +++ b/srcs/juloo.keyboard2/Keyboard2View.java @@ -90,6 +90,27 @@ public class Keyboard2View extends View invalidate(); } + /** Called by auto-capitalisation. */ + public void set_shift_state(boolean state) + { + KeyValue shift = KeyValue.getKeyByName("shift"); + KeyboardData.Key key = _keyboard.findKeyWithValue(shift); + if (key == null) + { + // Lookup again for the lockable shift key, which is a different value. + shift = shift.withFlags(shift.getFlags() | KeyValue.FLAG_LOCK); + key = _keyboard.findKeyWithValue(shift); + } + if (key != null) + { + if (state) + _pointers.add_fake_pointer(shift, key); + else + _pointers.remove_fake_pointer(shift, key); + invalidate(); + } + } + public KeyValue modifyKey(KeyValue k, Pointers.Modifiers mods) { return KeyModifier.modify(k, mods); diff --git a/srcs/juloo.keyboard2/KeyboardData.java b/srcs/juloo.keyboard2/KeyboardData.java index c95dc51..9b47152 100644 --- a/srcs/juloo.keyboard2/KeyboardData.java +++ b/srcs/juloo.keyboard2/KeyboardData.java @@ -43,6 +43,17 @@ class KeyboardData return new KeyboardData(rows, keysWidth, extra_keys); } + public Key findKeyWithValue(KeyValue kv) + { + for (Row r : rows) + { + Key k = r.findKeyWithValue(kv); + if (k != null) + return k; + } + return null; + } + private static void addExtraKeys_to_row(ArrayList rows, final Iterator extra_keys, int row_i, final int d) { if (!extra_keys.hasNext()) @@ -168,6 +179,14 @@ class KeyboardData public Key apply(Key k) { return k.scaleWidth(s); } }); } + + public Key findKeyWithValue(KeyValue kv) + { + for (Key k : keys) + if (k.hasValue(kv)) + return k; + return null; + } } public static class Key @@ -291,6 +310,17 @@ class KeyboardData } return (c == null) ? null : c.kv; } + + public boolean hasValue(KeyValue kv) + { + return (hasValue(key0, kv) || hasValue(key1, kv) || hasValue(key2, kv) || + hasValue(key3, kv) || hasValue(key4, kv)); + } + + private static boolean hasValue(Corner c, KeyValue kv) + { + return (c != null && c.kv.equals(kv)); + } } public static final class Corner diff --git a/srcs/juloo.keyboard2/Pointers.java b/srcs/juloo.keyboard2/Pointers.java index 9b8ee97..84323ae 100644 --- a/srcs/juloo.keyboard2/Pointers.java +++ b/srcs/juloo.keyboard2/Pointers.java @@ -74,6 +74,24 @@ public final class Pointers implements Handler.Callback return -1; } + /** Fake pointers are latched and not lockable. */ + public void add_fake_pointer(KeyValue kv, KeyboardData.Key key) + { + // Avoid adding a fake pointer to a key that is already down. + if (isKeyDown(key)) + return; + Pointer ptr = new Pointer(-1, key, kv, 0.f, 0.f, Modifiers.EMPTY); + ptr.flags = ptr.flags & ~(KeyValue.FLAG_LATCH | KeyValue.FLAG_LOCK); + _ptrs.add(ptr); + } + + public void remove_fake_pointer(KeyValue kv, KeyboardData.Key key) + { + Pointer ptr = getLatched(key, kv); + if (ptr != null) + removePtr(ptr); + } + // Receiving events public void onTouchUp(int pointerId) @@ -252,8 +270,11 @@ public final class Pointers implements Handler.Callback private Pointer getLatched(Pointer target) { - KeyboardData.Key k = target.key; - KeyValue v = target.value; + return getLatched(target.key, target.value); + } + + private Pointer getLatched(KeyboardData.Key k, KeyValue v) + { if (v == null) return null; for (Pointer p : _ptrs)