diff options
| author | Jules Aguillon | 2025-12-28 19:02:45 +0100 |
|---|---|---|
| committer | GitHub | 2025-12-28 19:02:45 +0100 |
| commit | 4d4c22b4e21505000d92fa3b6c1f400379c51cd4 (patch) | |
| tree | 0d8d7373575c4fc98cb342a4ab73f1192f700f4e /srcs | |
| parent | dfaf4dbb5766bf134cbf97d0516493e2256d2e5a (diff) | |
| parent | bdb05bcef060ee2aefab7663bb5441ea1398dd95 (diff) | |
| download | unexpected-keyboard-4d4c22b4e21505000d92fa3b6c1f400379c51cd4.tar.gz unexpected-keyboard-4d4c22b4e21505000d92fa3b6c1f400379c51cd4.zip | |
Merge pull request #1150 from Julow/candidates_view
Candidates view
Diffstat (limited to 'srcs')
| -rw-r--r-- | srcs/juloo.keyboard2/CandidatesView.java | 104 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/Config.java | 3 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/CurrentlyTypedWord.java | 149 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/EditorConfig.java | 9 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/KeyEventHandler.java | 50 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/Keyboard2.java | 37 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/Keyboard2View.java | 2 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/Logs.java | 2 | ||||
| -rw-r--r-- | srcs/juloo.keyboard2/Suggestions.java | 36 |
9 files changed, 376 insertions, 16 deletions
diff --git a/srcs/juloo.keyboard2/CandidatesView.java b/srcs/juloo.keyboard2/CandidatesView.java new file mode 100644 index 0000000..232ed23 --- /dev/null +++ b/srcs/juloo.keyboard2/CandidatesView.java @@ -0,0 +1,104 @@ +package juloo.keyboard2; + +import android.content.Context; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.LinearLayout; +import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; + +public class CandidatesView extends LinearLayout +{ + static final int NUM_CANDIDATES = 3; + + Config _config; + + /** Candidates currently visible. Entries can be [null] when there are less + than [NUM_CANDIDATES] suggestions. */ + String[] _items = new String[NUM_CANDIDATES]; + + /** Text views showing the candidates in [_items]. Text views visibility is + set to [GONE] when there are less than [NUM_CANDIDATES] suggestions. */ + TextView[] _item_views = new TextView[NUM_CANDIDATES]; + + public CandidatesView(Context context, AttributeSet attrs) + { + super(context, attrs); + _config = Config.globalConfig(); + } + + @Override + protected void onFinishInflate() + { + super.onFinishInflate(); + setup_item_view(0, R.id.candidates_middle); + setup_item_view(1, R.id.candidates_right); + setup_item_view(2, R.id.candidates_left); + } + + public void set_candidates(List<String> suggestions) + { + int s_count = suggestions.size(); + for (int i = 0; i < _item_views.length; i++) + { + TextView v = _item_views[i]; + if (i < s_count) + { + String it = suggestions.get(i); + _items[i] = it; + v.setText(it); + v.setVisibility(View.VISIBLE); + } + else + { + _items[i] = null; + v.setVisibility(View.GONE); + } + } + } + + private void setup_item_view(final int item_index, int item_id) + { + TextView v = (TextView)findViewById(item_id); + v.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View _v) + { + String it = _items[item_index]; + if (it != null) + _config.handler.suggestion_entered(it); + } + }); + v.setVisibility(View.GONE); + _item_views[item_index] = v; + } + + public static boolean should_show(EditorInfo info) + { + int variation = info.inputType & InputType.TYPE_MASK_VARIATION; + int flags = info.inputType & InputType.TYPE_MASK_FLAGS; + switch (info.inputType & InputType.TYPE_MASK_CLASS) + { + case InputType.TYPE_CLASS_TEXT: + switch (variation) + { + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + return false; + default: + if ((flags & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) + return false; // Editor requested that we don't show suggestions + return true; + } + case InputType.TYPE_CLASS_NUMBER: + // Beware of TYPE_NUMBER_VARIATION_PASSWORD + return false; + default: return false; + } + } +} diff --git a/srcs/juloo.keyboard2/Config.java b/srcs/juloo.keyboard2/Config.java index 64e2d87..775c206 100644 --- a/srcs/juloo.keyboard2/Config.java +++ b/srcs/juloo.keyboard2/Config.java @@ -75,6 +75,7 @@ public final class Config // Dynamically set /** Configuration options implied by the connected editor. */ public EditorConfig editor_config; + public boolean should_show_candidates_view; public boolean shouldOfferVoiceTyping; public ExtraKeys extra_keys_subtype; public Map<KeyValue, KeyboardData.PreferredPos> extra_keys_param; @@ -101,6 +102,7 @@ public final class Config // from prefs refresh(res, foldableUnfolded); // initialized later + should_show_candidates_view = false; shouldOfferVoiceTyping = false; extra_keys_subtype = null; handler = h; @@ -291,6 +293,7 @@ public final class Config public void key_down(KeyValue value, boolean is_swipe); public void key_up(KeyValue value, Pointers.Modifiers mods); public void mods_changed(Pointers.Modifiers mods); + public void suggestion_entered(String text); } /** Config migrations. */ diff --git a/srcs/juloo.keyboard2/CurrentlyTypedWord.java b/srcs/juloo.keyboard2/CurrentlyTypedWord.java new file mode 100644 index 0000000..87a1cf1 --- /dev/null +++ b/srcs/juloo.keyboard2/CurrentlyTypedWord.java @@ -0,0 +1,149 @@ +package juloo.keyboard2; + +import android.os.Handler; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import java.util.List; + +/** Keep track of the word being typed. */ +public final class CurrentlyTypedWord +{ + InputConnection _ic = null; + Handler _handler; + Callback _callback; + + /** The currently typed word. */ + StringBuilder _w = new StringBuilder(); + /** This can be disabled if the editor doesn't support looking at the text + before the cursor. */ + boolean _enabled = false; + /** The current word is empty while the selection is ongoing. */ + boolean _has_selection = false; + /** Used to avoid concurrent refreshes in [delayed_refresh()]. */ + boolean _refresh_pending = false; + + /** The estimated cursor position. Used to avoid expensive IPC calls when the + typed word can be estimated locally with [typed]. When the cursor + position gets out of sync, the text before the cursor is queried again to + the editor. */ + int _cursor; + + public CurrentlyTypedWord(Handler h, Callback cb) + { + _handler = h; + _callback = cb; + } + + public String get() + { + return _w.toString(); + } + + public void started(Config conf, InputConnection ic) + { + _ic = ic; + _enabled = true; + EditorConfig e = conf.editor_config; + _has_selection = e.initial_sel_start != e.initial_sel_end; + _cursor = e.initial_sel_start; + if (!_has_selection) + set_current_word(e.initial_text_before_cursor); + } + + public void typed(String s) + { + if (!_enabled) + return; + _has_selection = false; + type_chars(s); + callback(); + } + + public void selection_updated(int oldSelStart, int newSelStart, int newSelEnd) + { + // Avoid the expensive [refresh_current_word] call when [typed] was called + // before. + boolean new_has_sel = newSelStart != newSelEnd; + if (!_enabled || (newSelStart == _cursor && new_has_sel == _has_selection)) + return; + _has_selection = new_has_sel; + _cursor = newSelStart; + refresh_current_word(); + } + + public void event_sent(int code, int meta) + { + if (!_enabled) + return; + delayed_refresh(); + } + + void callback() + { + _callback.currently_typed_word(_w.toString()); + } + + /** Estimate the currently typed word after [chars] has been typed. */ + void type_chars(String s) + { + int len = s.length(); + for (int i = 0; i < len;) + { + int c = s.codePointAt(i); + if (Character.isLetter(c)) + _w.appendCodePoint(c); + else + _w.setLength(0); + _cursor++; + i += Character.charCount(c); + } + } + + /** Refresh the current word by immediately querying the editor. */ + void refresh_current_word() + { + _refresh_pending = false; + if (_has_selection) + set_current_word(""); + else + set_current_word(_ic.getTextBeforeCursor(10, 0)); + } + + /** Refresh the current word by immediately querying the editor. */ + void set_current_word(CharSequence text_before_cursor) + { + if (text_before_cursor == null) + { + _enabled = false; + return; + } + _w.setLength(0); + int saved_cursor = _cursor; + type_chars(text_before_cursor.toString()); + _cursor = saved_cursor; + callback(); + } + + /** Wait some time to let the editor finishes reacting to changes and call + [refresh_current_word]. */ + void delayed_refresh() + { + _refresh_pending = true; + _handler.postDelayed(delayed_refresh_run, 50); + } + + Runnable delayed_refresh_run = new Runnable() + { + public void run() + { + if (_refresh_pending) + refresh_current_word(); + } + }; + + public static interface Callback + { + public void currently_typed_word(String word); + } +} diff --git a/srcs/juloo.keyboard2/EditorConfig.java b/srcs/juloo.keyboard2/EditorConfig.java index d9f1ae9..bdfab3c 100644 --- a/srcs/juloo.keyboard2/EditorConfig.java +++ b/srcs/juloo.keyboard2/EditorConfig.java @@ -26,6 +26,11 @@ public final class EditorConfig // Whether caps state should be updated right away. public boolean caps_initially_updated = false; + /** CurrentlyTypedWord. */ + public CharSequence initial_text_before_cursor = null; + public int initial_sel_start; + public int initial_sel_end; + public EditorConfig() {} public void refresh(EditorInfo info, Resources res) @@ -66,6 +71,10 @@ public final class EditorConfig caps_mode = info.inputType & TextUtils.CAP_MODE_SENTENCES; caps_initially_enabled = (info.initialCapsMode != 0); caps_initially_updated = caps_should_update_state(info); + /* CurrentlyTypedWord */ + initial_text_before_cursor = info.getInitialTextBeforeCursor(10, 0); + initial_sel_start = info.initialSelStart; + initial_sel_end = info.initialSelEnd; } String actionLabel_of_imeAction(int action, Resources res) diff --git a/srcs/juloo.keyboard2/KeyEventHandler.java b/srcs/juloo.keyboard2/KeyEventHandler.java index fc3a641..17c7d7b 100644 --- a/srcs/juloo.keyboard2/KeyEventHandler.java +++ b/srcs/juloo.keyboard2/KeyEventHandler.java @@ -12,10 +12,13 @@ import java.util.Iterator; public final class KeyEventHandler implements Config.IKeyEventHandler, - ClipboardHistoryService.ClipboardPasteCallback + ClipboardHistoryService.ClipboardPasteCallback, + CurrentlyTypedWord.Callback { IReceiver _recv; Autocapitalisation _autocap; + Suggestions _suggestions; + CurrentlyTypedWord _typedword; /** State of the system modifiers. It is updated whether a modifier is down or up and a corresponding key event is sent. */ Pointers.Modifiers _mods; @@ -30,23 +33,29 @@ public final class KeyEventHandler public KeyEventHandler(IReceiver recv) { _recv = recv; - _autocap = new Autocapitalisation(recv.getHandler(), + Handler handler = recv.getHandler(); + _autocap = new Autocapitalisation(handler, this.new Autocapitalisation_callback()); _mods = Pointers.Modifiers.EMPTY; + _suggestions = new Suggestions(recv); + _typedword = new CurrentlyTypedWord(handler, this); } /** Editing just started. */ public void started(Config conf) { - _autocap.started(conf, _recv.getCurrentInputConnection()); + InputConnection ic = _recv.getCurrentInputConnection(); + _autocap.started(conf, ic); + _typedword.started(conf, ic); _move_cursor_force_fallback = conf.editor_config.should_move_cursor_force_fallback; } /** Selection has been updated. */ - public void selection_updated(int oldSelStart, int newSelStart) + public void selection_updated(int oldSelStart, int newSelStart, int newSelEnd) { _autocap.selection_updated(oldSelStart, newSelStart); + _typedword.selection_updated(oldSelStart, newSelStart, newSelEnd); } /** A key is being pressed. There will not necessarily be a corresponding @@ -111,11 +120,23 @@ public final class KeyEventHandler } @Override + public void suggestion_entered(String text) + { + replace_text_before_cursor(_typedword.get().length(), text + " "); + } + + @Override public void paste_from_clipboard_pane(String content) { send_text(content); } + @Override + public void currently_typed_word(String word) + { + _suggestions.currently_typed_word(word); + } + /** Update [_mods] to be consistent with the [mods], sending key events if needed. */ void update_meta_state(Pointers.Modifiers mods) @@ -202,16 +223,31 @@ public final class KeyEventHandler metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); if (eventAction == KeyEvent.ACTION_UP) + { _autocap.event_sent(eventCode, metaState); + _typedword.event_sent(eventCode, metaState); + } } - void send_text(CharSequence text) + void send_text(String text) { InputConnection conn = _recv.getCurrentInputConnection(); if (conn == null) return; - conn.commitText(text, 1); _autocap.typed(text); + _typedword.typed(text); + conn.commitText(text, 1); + } + + void replace_text_before_cursor(int remove_length, String new_text) + { + InputConnection conn = _recv.getCurrentInputConnection(); + if (conn == null) + return; + conn.beginBatchEdit(); + conn.deleteSurroundingText(remove_length, 0); + conn.commitText(new_text, 1); + conn.endBatchEdit(); } /** See {!InputConnection.performContextMenuAction}. */ @@ -467,7 +503,7 @@ public final class KeyEventHandler return (conn.getSelectedText(0) != null); } - public static interface IReceiver + public static interface IReceiver extends Suggestions.Callback { public void handle_event_key(KeyValue.Event ev); public void set_shift_state(boolean state, boolean lock); diff --git a/srcs/juloo.keyboard2/Keyboard2.java b/srcs/juloo.keyboard2/Keyboard2.java index 1efe0f5..a2940d5 100644 --- a/srcs/juloo.keyboard2/Keyboard2.java +++ b/srcs/juloo.keyboard2/Keyboard2.java @@ -29,7 +29,10 @@ import juloo.keyboard2.prefs.LayoutsPreference; public class Keyboard2 extends InputMethodService implements SharedPreferences.OnSharedPreferenceChangeListener { + /** The view containing the keyboard and candidates view. */ + private ViewGroup _container_view; private Keyboard2View _keyboardView; + private CandidatesView _candidates_view; private KeyEventHandler _keyeventhandler; /** If not 'null', the layout to use instead of [_config.current_layout]. */ private KeyboardData _currentSpecialLayout; @@ -115,9 +118,8 @@ public class Keyboard2 extends InputMethodService Config.initGlobalConfig(prefs, getResources(), _keyeventhandler, _foldStateTracker.isUnfolded()); prefs.registerOnSharedPreferenceChangeListener(this); _config = Config.globalConfig(); - _keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard); - _keyboardView.reset(); Logs.set_debug_logs(getResources().getBoolean(R.bool.debug_logs)); + create_keyboard_view(); ClipboardHistoryService.on_startup(this, _keyeventhandler); _foldStateTracker.setChangedCallback(() -> { refresh_config(); }); } @@ -129,6 +131,13 @@ public class Keyboard2 extends InputMethodService _foldStateTracker.close(); } + private void create_keyboard_view() + { + _container_view = (ViewGroup)inflate_view(R.layout.keyboard); + _keyboardView = (Keyboard2View)_container_view.findViewById(R.id.keyboard_view); + _candidates_view = (CandidatesView)_container_view.findViewById(R.id.candidates_view); + } + private List<InputMethodSubtype> getEnabledSubtypes(InputMethodManager imm) { String pkg = getPackageName(); @@ -198,6 +207,13 @@ public class Keyboard2 extends InputMethodService _localeTextLayout = default_layout; } + private void refresh_candidates_view(EditorInfo info) + { + boolean should_show = CandidatesView.should_show(info); + _config.should_show_candidates_view = should_show; + _candidates_view.setVisibility(should_show ? View.VISIBLE : View.GONE); + } + /** Might re-create the keyboard view. [_keyboardView.setKeyboard()] and [setInputView()] must be called soon after. */ private void refresh_config() @@ -208,11 +224,13 @@ public class Keyboard2 extends InputMethodService // Refreshing the theme config requires re-creating the views if (prev_theme != _config.theme) { - _keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard); + create_keyboard_view(); _emojiPane = null; _clipboard_pane = null; - setInputView(_keyboardView); + setInputView(_container_view); } + // Set keyboard background opacity + _container_view.getBackground().setAlpha(_config.keyboardOpacity); _keyboardView.reset(); } @@ -234,10 +252,11 @@ public class Keyboard2 extends InputMethodService { _config.editor_config.refresh(info, getResources()); refresh_config(); + refresh_candidates_view(info); _currentSpecialLayout = refresh_special_layout(); _keyboardView.setKeyboard(current_layout()); _keyeventhandler.started(_config); - setInputView(_keyboardView); + setInputView(_container_view); Logs.debug_startup_input_view(info, _config); } @@ -252,7 +271,6 @@ public class Keyboard2 extends InputMethodService v.requestApplyInsets(); } - @Override public void updateFullscreenMode() { super.updateFullscreenMode(); @@ -328,7 +346,7 @@ public class Keyboard2 extends InputMethodService public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); - _keyeventhandler.selection_updated(oldSelStart, newSelStart); + _keyeventhandler.selection_updated(oldSelStart, newSelStart, newSelEnd); if ((oldSelStart == oldSelEnd) != (newSelStart == newSelEnd)) _keyboardView.set_selection_state(newSelStart != newSelEnd); } @@ -463,6 +481,11 @@ public class Keyboard2 extends InputMethodService { return _handler; } + + public void set_suggestions(List<String> suggestions) + { + _candidates_view.set_candidates(suggestions); + } } private IBinder getConnectionToken() diff --git a/srcs/juloo.keyboard2/Keyboard2View.java b/srcs/juloo.keyboard2/Keyboard2View.java index 58c689d..068ef67 100644 --- a/srcs/juloo.keyboard2/Keyboard2View.java +++ b/srcs/juloo.keyboard2/Keyboard2View.java @@ -344,8 +344,6 @@ public class Keyboard2View extends View @Override protected void onDraw(Canvas canvas) { - // Set keyboard background opacity - getBackground().setAlpha(_config.keyboardOpacity); float y = _tc.margin_top; for (KeyboardData.Row row : _keyboard.rows) { diff --git a/srcs/juloo.keyboard2/Logs.java b/srcs/juloo.keyboard2/Logs.java index 1bef51c..a956305 100644 --- a/srcs/juloo.keyboard2/Logs.java +++ b/srcs/juloo.keyboard2/Logs.java @@ -26,6 +26,8 @@ public final class Logs _debug_logs.println("swapEnterActionKey: " +conf.editor_config.swapEnterActionKey); _debug_logs.println("actionLabel: "+conf.editor_config.actionLabel); + _debug_logs.println("should_show_candidates_view: " + +conf.should_show_candidates_view); } public static void debug_config_migration(int from_version, int to_version) diff --git a/srcs/juloo.keyboard2/Suggestions.java b/srcs/juloo.keyboard2/Suggestions.java new file mode 100644 index 0000000..4c0c97a --- /dev/null +++ b/srcs/juloo.keyboard2/Suggestions.java @@ -0,0 +1,36 @@ +package juloo.keyboard2; + +import java.util.List; +import java.util.Arrays; + +/** Keep track of the word being typed and provide suggestions for + [CandidatesView]. */ +public final class Suggestions +{ + Callback _callback; + + public Suggestions(Callback c) + { + _callback = c; + } + + public void currently_typed_word(String word) + { + if (word.equals("")) + { + _callback.set_suggestions(NO_SUGGESTIONS); + } + else + { + // TODO + _callback.set_suggestions(Arrays.asList(word)); + } + } + + static final List<String> NO_SUGGESTIONS = Arrays.asList(); + + public static interface Callback + { + public void set_suggestions(List<String> suggestions); + } +} |
