When the layout is split, insert a middle column showing the complete_first, complete_second, etc.. keys. These keys are rendered without borders and background with a new role. The suggestion bar is hidden for split layouts. The intention is to gain much needed vertical space.
554 lines
17 KiB
Java
554 lines
17 KiB
Java
package juloo.keyboard2;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.res.Configuration;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.inputmethodservice.InputMethodService;
|
|
import android.os.Build.VERSION;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.text.InputType;
|
|
import android.util.Log;
|
|
import android.util.LogPrinter;
|
|
import android.view.*;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.inputmethod.InputConnection;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.view.inputmethod.InputMethodSubtype;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.LinearLayout;
|
|
import java.util.AbstractMap.SimpleEntry;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import juloo.cdict.Cdict;
|
|
import juloo.keyboard2.dict.Dictionaries;
|
|
import juloo.keyboard2.dict.DictionariesActivity;
|
|
import juloo.keyboard2.prefs.LayoutsPreference;
|
|
import juloo.keyboard2.suggestions.CandidatesView;
|
|
import juloo.keyboard2.suggestions.Suggestions;
|
|
|
|
public class Keyboard2 extends InputMethodService
|
|
implements SharedPreferences.OnSharedPreferenceChangeListener
|
|
{
|
|
/** The view containing the keyboard and candidates view. */
|
|
private ViewGroup _keyboard_container_view;
|
|
private Keyboard2View _keyboard_layout_view;
|
|
private CandidatesView _candidates_view;
|
|
private Suggestions _suggestions;
|
|
private KeyEventHandler _keyeventhandler;
|
|
/** If not 'null', the layout to use instead of [_config.current_layout]. */
|
|
private KeyboardData _currentSpecialLayout;
|
|
/** Layout associated with the currently selected locale. Not 'null'. */
|
|
private KeyboardData _localeTextLayout;
|
|
/** Installed and current locales. */
|
|
private Dictionaries _dictionaries;
|
|
private ViewGroup _emojiPane = null;
|
|
private ViewGroup _clipboard_pane = null;
|
|
private Handler _handler;
|
|
|
|
private Config _config;
|
|
|
|
private FoldStateTracker _foldStateTracker;
|
|
|
|
/** Layout currently visible before it has been modified. */
|
|
KeyboardData current_layout_unmodified()
|
|
{
|
|
if (_currentSpecialLayout != null)
|
|
return _currentSpecialLayout;
|
|
KeyboardData layout = null;
|
|
int layout_i = _config.get_current_layout();
|
|
if (layout_i >= _config.layouts.size())
|
|
layout_i = 0;
|
|
if (layout_i < _config.layouts.size())
|
|
layout = _config.layouts.get(layout_i);
|
|
if (layout == null)
|
|
layout = _localeTextLayout;
|
|
return layout;
|
|
}
|
|
|
|
/** Layout currently visible. */
|
|
KeyboardData current_layout()
|
|
{
|
|
if (_currentSpecialLayout != null)
|
|
return _currentSpecialLayout;
|
|
return LayoutModifier.modify_layout(current_layout_unmodified());
|
|
}
|
|
|
|
void setTextLayout(int l)
|
|
{
|
|
_config.set_current_layout(l);
|
|
_currentSpecialLayout = null;
|
|
_keyboard_layout_view.setKeyboard(current_layout());
|
|
}
|
|
|
|
void incrTextLayout(int delta)
|
|
{
|
|
int s = _config.layouts.size();
|
|
setTextLayout((_config.get_current_layout() + delta + s) % s);
|
|
}
|
|
|
|
void setSpecialLayout(KeyboardData l)
|
|
{
|
|
_currentSpecialLayout = l;
|
|
_keyboard_layout_view.setKeyboard(l);
|
|
}
|
|
|
|
KeyboardData loadLayout(int layout_id)
|
|
{
|
|
return KeyboardData.load(getResources(), layout_id);
|
|
}
|
|
|
|
/** Load a layout that contains a numpad. */
|
|
KeyboardData loadNumpad(int layout_id)
|
|
{
|
|
return LayoutModifier.modify_numpad(KeyboardData.load(getResources(), layout_id),
|
|
current_layout_unmodified());
|
|
}
|
|
|
|
KeyboardData loadNumericLayout()
|
|
{
|
|
return loadNumpad(_config.orientation_landscape ?
|
|
R.xml.numeric_landscape : R.xml.numeric);
|
|
}
|
|
|
|
KeyboardData loadPinentry(int layout_id)
|
|
{
|
|
return LayoutModifier.modify_pinentry(KeyboardData.load(getResources(), layout_id),
|
|
current_layout_unmodified());
|
|
}
|
|
|
|
@Override
|
|
public void onCreate()
|
|
{
|
|
super.onCreate();
|
|
SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this);
|
|
_handler = new Handler(getMainLooper());
|
|
_foldStateTracker = new FoldStateTracker(this);
|
|
_dictionaries = Dictionaries.instance(this);
|
|
Config.initGlobalConfig(prefs, getResources(),
|
|
_foldStateTracker.isUnfolded(), _dictionaries);
|
|
_config = Config.globalConfig();
|
|
Receiver recvr = this.new Receiver();
|
|
_suggestions = new Suggestions(recvr, _config);
|
|
_keyeventhandler = new KeyEventHandler(recvr, _suggestions);
|
|
KeyValue.Stateful._handler = recvr;
|
|
_config.handler = _keyeventhandler;
|
|
prefs.registerOnSharedPreferenceChangeListener(this);
|
|
Logs.set_debug_logs(getResources().getBoolean(R.bool.debug_logs));
|
|
refreshSubtypeImm();
|
|
create_keyboard_view();
|
|
ClipboardHistoryService.on_startup(this, _keyeventhandler);
|
|
_foldStateTracker.setChangedCallback(() -> { refresh_config(); });
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
|
|
_foldStateTracker.close();
|
|
}
|
|
|
|
private void create_keyboard_view()
|
|
{
|
|
_keyboard_container_view = (ViewGroup)inflate_view(R.layout.keyboard);
|
|
_keyboard_layout_view = (Keyboard2View)_keyboard_container_view.findViewById(R.id.keyboard_view);
|
|
_candidates_view = (CandidatesView)_keyboard_container_view.findViewById(R.id.candidates_view);
|
|
}
|
|
|
|
InputMethodManager get_imm()
|
|
{
|
|
return (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
|
|
}
|
|
|
|
private void refreshSubtypeImm()
|
|
{
|
|
_config.shouldOfferVoiceTyping = true;
|
|
KeyboardData default_layout = null;
|
|
_config.device_locales = DeviceLocales.load(this);
|
|
if (_config.device_locales.default_ != null)
|
|
{
|
|
String layout_name = _config.device_locales.default_.default_layout;
|
|
if (layout_name != null)
|
|
default_layout = LayoutsPreference.layout_of_string(getResources(), layout_name);
|
|
}
|
|
_config.extra_keys_subtype = _config.device_locales.extra_keys();
|
|
if (default_layout == null)
|
|
default_layout = loadLayout(R.xml.latn_qwerty_us);
|
|
_localeTextLayout = default_layout;
|
|
}
|
|
|
|
private void refresh_current_dictionary()
|
|
{
|
|
_config.current_dictionary = null;
|
|
_config.emoji_dictionary = null;
|
|
if (_config.device_locales.default_ == null)
|
|
return;
|
|
String current = _config.device_locales.default_.dictionary;
|
|
if (current == null)
|
|
return;
|
|
Cdict[] dicts = _dictionaries.load(current);
|
|
if (dicts == null)
|
|
return;
|
|
_config.current_dictionary = Dictionaries.find_by_name(dicts, "main");
|
|
_config.emoji_dictionary = Dictionaries.find_by_name(dicts, "emoji");
|
|
}
|
|
|
|
private void refresh_candidates_view()
|
|
{
|
|
boolean should_show =
|
|
_config.suggestions_enabled
|
|
&& _config.editor_config.should_show_candidates_view
|
|
&& !_config.split_layout;
|
|
if (should_show)
|
|
_candidates_view.refresh_config(_config);
|
|
_candidates_view.setVisibility(should_show ? View.VISIBLE : View.GONE);
|
|
}
|
|
|
|
/** Might re-create the keyboard view. [_keyboard_layout_view.setKeyboard()] and
|
|
[setInputView()] must be called soon after. */
|
|
private void refresh_config()
|
|
{
|
|
int prev_theme = _config.theme;
|
|
_config.refresh(getResources(), _foldStateTracker.isUnfolded(), _dictionaries);
|
|
refresh_current_dictionary();
|
|
// Refreshing the theme config requires re-creating the views
|
|
if (prev_theme != _config.theme)
|
|
{
|
|
create_keyboard_view();
|
|
_emojiPane = null;
|
|
_clipboard_pane = null;
|
|
setInputView(_keyboard_container_view);
|
|
}
|
|
// Set keyboard background opacity
|
|
Drawable bg = _keyboard_container_view.getBackground().mutate();
|
|
bg.setAlpha(_config.keyboardOpacity);
|
|
_keyboard_container_view.setBackground(bg);
|
|
_keyboard_layout_view.reset();
|
|
refresh_candidates_view();
|
|
}
|
|
|
|
private KeyboardData refresh_special_layout()
|
|
{
|
|
if (_config.editor_config.numeric_layout)
|
|
{
|
|
switch (_config.selected_number_layout)
|
|
{
|
|
case PIN:
|
|
return loadPinentry(_config.orientation_landscape ?
|
|
R.xml.pin_landscape : R.xml.pin);
|
|
case NUMBER:
|
|
return loadNumericLayout();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void onStartInputView(EditorInfo info, boolean restarting)
|
|
{
|
|
_config.editor_config.refresh(info, getResources());
|
|
refresh_config();
|
|
_currentSpecialLayout = refresh_special_layout();
|
|
_keyboard_layout_view.setKeyboard(current_layout());
|
|
_keyeventhandler.started(_config);
|
|
setInputView(_keyboard_container_view);
|
|
Logs.debug_startup_input_view(info, _config);
|
|
}
|
|
|
|
@Override
|
|
public void setInputView(View v)
|
|
{
|
|
ViewParent parent = v.getParent();
|
|
if (parent != null && parent instanceof ViewGroup)
|
|
((ViewGroup)parent).removeView(v);
|
|
super.setInputView(v);
|
|
updateSoftInputWindowLayoutParams();
|
|
v.requestApplyInsets();
|
|
}
|
|
|
|
@Override
|
|
public void updateFullscreenMode() {
|
|
super.updateFullscreenMode();
|
|
updateSoftInputWindowLayoutParams();
|
|
}
|
|
|
|
private void updateSoftInputWindowLayoutParams() {
|
|
final Window window = getWindow().getWindow();
|
|
// On API >= 35, Keyboard2View behaves as edge-to-edge
|
|
// APIs 30 to 34 have visual artifact when edge-to-edge is enabled
|
|
if (VERSION.SDK_INT >= 35)
|
|
{
|
|
WindowManager.LayoutParams wattrs = window.getAttributes();
|
|
wattrs.layoutInDisplayCutoutMode =
|
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
|
// Allow to draw behind system bars
|
|
wattrs.setFitInsetsTypes(0);
|
|
window.setDecorFitsSystemWindows(false);
|
|
}
|
|
updateLayoutHeightOf(window, ViewGroup.LayoutParams.MATCH_PARENT);
|
|
final View inputArea = window.findViewById(android.R.id.inputArea);
|
|
|
|
updateLayoutHeightOf(
|
|
(View) inputArea.getParent(),
|
|
isFullscreenMode()
|
|
? ViewGroup.LayoutParams.MATCH_PARENT
|
|
: ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
updateLayoutGravityOf((View) inputArea.getParent(), Gravity.BOTTOM);
|
|
|
|
}
|
|
|
|
private static void updateLayoutHeightOf(final Window window, final int layoutHeight) {
|
|
final WindowManager.LayoutParams params = window.getAttributes();
|
|
if (params != null && params.height != layoutHeight) {
|
|
params.height = layoutHeight;
|
|
window.setAttributes(params);
|
|
}
|
|
}
|
|
|
|
private static void updateLayoutHeightOf(final View view, final int layoutHeight) {
|
|
final ViewGroup.LayoutParams params = view.getLayoutParams();
|
|
if (params != null && params.height != layoutHeight) {
|
|
params.height = layoutHeight;
|
|
view.setLayoutParams(params);
|
|
}
|
|
}
|
|
|
|
private static void updateLayoutGravityOf(final View view, final int layoutGravity) {
|
|
final ViewGroup.LayoutParams lp = view.getLayoutParams();
|
|
if (lp instanceof LinearLayout.LayoutParams) {
|
|
final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) lp;
|
|
if (params.gravity != layoutGravity) {
|
|
params.gravity = layoutGravity;
|
|
view.setLayoutParams(params);
|
|
}
|
|
} else if (lp instanceof FrameLayout.LayoutParams) {
|
|
final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) lp;
|
|
if (params.gravity != layoutGravity) {
|
|
params.gravity = layoutGravity;
|
|
view.setLayoutParams(params);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype)
|
|
{
|
|
refreshSubtypeImm();
|
|
refresh_current_dictionary();
|
|
refresh_candidates_view();
|
|
_keyboard_layout_view.setKeyboard(current_layout());
|
|
_keyeventhandler.ime_subtype_changed();
|
|
}
|
|
|
|
@Override
|
|
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, newSelEnd);
|
|
if ((oldSelStart == oldSelEnd) != (newSelStart == newSelEnd))
|
|
_keyboard_layout_view.set_selection_state(newSelStart != newSelEnd);
|
|
}
|
|
|
|
@Override
|
|
public void onFinishInputView(boolean finishingInput)
|
|
{
|
|
super.onFinishInputView(finishingInput);
|
|
_keyboard_layout_view.reset();
|
|
}
|
|
|
|
@Override
|
|
public void onSharedPreferenceChanged(SharedPreferences _prefs, String _key)
|
|
{
|
|
refresh_config();
|
|
_keyboard_layout_view.setKeyboard(current_layout());
|
|
}
|
|
|
|
@Override
|
|
public boolean onEvaluateFullscreenMode()
|
|
{
|
|
/* Entirely disable fullscreen mode. */
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onEvaluateInputViewShown()
|
|
{
|
|
// Since Android 16, this method returns [false] for unknown reasons.
|
|
if (super.onEvaluateInputViewShown())
|
|
return true;
|
|
if (getResources().getConfiguration().hardKeyboardHidden
|
|
== Configuration.HARDKEYBOARDHIDDEN_NO
|
|
&& _config.physical_keyboard_hide)
|
|
{
|
|
Logs.debug("Physical keyboard is present");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Called from [onClick] attributes. */
|
|
public void launch_dictionaries_activity(View v)
|
|
{
|
|
start_activity(DictionariesActivity.class);
|
|
}
|
|
|
|
void start_activity(Class cls)
|
|
{
|
|
Intent intent = new Intent(this, cls);
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
startActivity(intent);
|
|
}
|
|
|
|
/** Not static */
|
|
public class Receiver implements KeyEventHandler.IReceiver,
|
|
KeyValue.Stateful.Symbol_provider
|
|
{
|
|
public void handle_event_key(KeyValue.Event ev)
|
|
{
|
|
switch (ev)
|
|
{
|
|
case CONFIG:
|
|
start_activity(SettingsActivity.class);
|
|
break;
|
|
|
|
case SWITCH_TEXT:
|
|
_currentSpecialLayout = null;
|
|
_keyboard_layout_view.setKeyboard(current_layout());
|
|
break;
|
|
|
|
case SWITCH_NUMERIC:
|
|
setSpecialLayout(loadNumericLayout());
|
|
break;
|
|
|
|
case SWITCH_EMOJI:
|
|
if (_emojiPane == null)
|
|
_emojiPane = (ViewGroup)inflate_view(R.layout.emoji_pane);
|
|
setInputView(_emojiPane);
|
|
break;
|
|
|
|
case SWITCH_CLIPBOARD:
|
|
if (_clipboard_pane == null)
|
|
_clipboard_pane = (ViewGroup)inflate_view(R.layout.clipboard_pane);
|
|
setInputView(_clipboard_pane);
|
|
break;
|
|
|
|
case SWITCH_BACK_EMOJI:
|
|
case SWITCH_BACK_CLIPBOARD:
|
|
setInputView(_keyboard_container_view);
|
|
break;
|
|
|
|
case CHANGE_METHOD_PICKER:
|
|
get_imm().showInputMethodPicker();
|
|
break;
|
|
|
|
case CHANGE_METHOD_PREV:
|
|
if (VERSION.SDK_INT < 28)
|
|
get_imm().switchToLastInputMethod(getConnectionToken());
|
|
else
|
|
switchToPreviousInputMethod();
|
|
break;
|
|
|
|
case CHANGE_METHOD_NEXT:
|
|
if (VERSION.SDK_INT < 28)
|
|
get_imm().switchToNextInputMethod(getConnectionToken(), false);
|
|
else
|
|
switchToNextInputMethod(false);
|
|
break;
|
|
|
|
case ACTION:
|
|
InputConnection conn = getCurrentInputConnection();
|
|
if (conn != null)
|
|
conn.performEditorAction(_config.editor_config.actionId);
|
|
break;
|
|
|
|
case SWITCH_FORWARD:
|
|
incrTextLayout(1);
|
|
break;
|
|
|
|
case SWITCH_BACKWARD:
|
|
incrTextLayout(-1);
|
|
break;
|
|
|
|
case SWITCH_GREEKMATH:
|
|
setSpecialLayout(loadNumpad(R.xml.greekmath));
|
|
break;
|
|
|
|
case CAPS_LOCK:
|
|
set_shift_state(true, true);
|
|
break;
|
|
|
|
case SWITCH_VOICE_TYPING:
|
|
if (!VoiceImeSwitcher.switch_to_voice_ime(Keyboard2.this, get_imm(),
|
|
Config.globalPrefs()))
|
|
_config.shouldOfferVoiceTyping = false;
|
|
break;
|
|
|
|
case SWITCH_VOICE_TYPING_CHOOSER:
|
|
VoiceImeSwitcher.choose_voice_ime(Keyboard2.this, get_imm(),
|
|
Config.globalPrefs());
|
|
break;
|
|
case HIDE_SELF:
|
|
Keyboard2.this.requestHideSelf(0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void set_shift_state(boolean state, boolean lock)
|
|
{
|
|
_keyboard_layout_view.set_shift_state(state, lock);
|
|
}
|
|
|
|
public void set_compose_pending(boolean pending)
|
|
{
|
|
_keyboard_layout_view.set_compose_pending(pending);
|
|
}
|
|
|
|
public void selection_state_changed(boolean selection_is_ongoing)
|
|
{
|
|
_keyboard_layout_view.set_selection_state(selection_is_ongoing);
|
|
}
|
|
|
|
public InputConnection getCurrentInputConnection()
|
|
{
|
|
return Keyboard2.this.getCurrentInputConnection();
|
|
}
|
|
|
|
public Handler getHandler()
|
|
{
|
|
return _handler;
|
|
}
|
|
|
|
public void set_suggestions(Suggestions suggestions)
|
|
{
|
|
_candidates_view.set_candidates(suggestions);
|
|
}
|
|
|
|
public String provide_stateful_key_symbol(KeyValue.Stateful q)
|
|
{
|
|
switch (q)
|
|
{
|
|
case Complete_first: return _suggestions.suggestions[0];
|
|
case Complete_second: return _suggestions.suggestions[1];
|
|
case Complete_third: return _suggestions.suggestions[2];
|
|
case Complete_emoji: return _suggestions.emoji_suggestion;
|
|
}
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private IBinder getConnectionToken()
|
|
{
|
|
return getWindow().getWindow().getAttributes().token;
|
|
}
|
|
|
|
private View inflate_view(int layout)
|
|
{
|
|
return View.inflate(new ContextThemeWrapper(this, _config.theme), layout, null);
|
|
}
|
|
}
|