diff --git a/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt b/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt
index ee8d677b0b..a4ecab3946 100644
--- a/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt
+++ b/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt
@@ -32,7 +32,7 @@ class EditorInstance(private val ims: InputMethodService) {
}
}
val textInputManager: TextInputManager
- get() = (ims as Trime).textInputManager
+ get() = (ims as Trime).textInputManager ?: error("TextInputManager is null")
var lastCommittedText: CharSequence = ""
diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java
deleted file mode 100644
index 33132ecbca..0000000000
--- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java
+++ /dev/null
@@ -1,1260 +0,0 @@
-/*
- * Copyright (C) 2015-present, osfans
- * waxaca@163.com https://github.com/osfans
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.osfans.trime.ime.core;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.inputmethodservice.InputMethodService;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.inputmethod.CursorAnchorInfo;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.osfans.trime.BuildConfig;
-import com.osfans.trime.R;
-import com.osfans.trime.core.Rime;
-import com.osfans.trime.data.AppPrefs;
-import com.osfans.trime.data.db.DraftHelper;
-import com.osfans.trime.data.sound.SoundThemeManager;
-import com.osfans.trime.data.theme.Theme;
-import com.osfans.trime.databinding.CompositionRootBinding;
-import com.osfans.trime.ime.broadcast.IntentReceiver;
-import com.osfans.trime.ime.enums.Keycode;
-import com.osfans.trime.ime.enums.SymbolKeyboardType;
-import com.osfans.trime.ime.keyboard.Event;
-import com.osfans.trime.ime.keyboard.InitializationUi;
-import com.osfans.trime.ime.keyboard.InputFeedbackManager;
-import com.osfans.trime.ime.keyboard.Key;
-import com.osfans.trime.ime.keyboard.Keyboard;
-import com.osfans.trime.ime.keyboard.KeyboardSwitcher;
-import com.osfans.trime.ime.keyboard.KeyboardView;
-import com.osfans.trime.ime.keyboard.KeyboardWindow;
-import com.osfans.trime.ime.lifecycle.LifecycleInputMethodService;
-import com.osfans.trime.ime.symbol.LiquidKeyboard;
-import com.osfans.trime.ime.symbol.TabManager;
-import com.osfans.trime.ime.symbol.TabView;
-import com.osfans.trime.ime.text.Candidate;
-import com.osfans.trime.ime.text.Composition;
-import com.osfans.trime.ime.text.CompositionPopupWindow;
-import com.osfans.trime.ime.text.ScrollView;
-import com.osfans.trime.ime.text.TextInputManager;
-import com.osfans.trime.ime.util.UiUtil;
-import com.osfans.trime.util.DimensionsKt;
-import com.osfans.trime.util.ShortcutUtils;
-import com.osfans.trime.util.StringUtils;
-import com.osfans.trime.util.ViewUtils;
-import com.osfans.trime.util.WeakHashSet;
-import java.util.Objects;
-import splitties.bitflags.BitFlagsKt;
-import splitties.systemservices.SystemServicesKt;
-import timber.log.Timber;
-
-/** {@link InputMethodService 輸入法}主程序 */
-public class Trime extends LifecycleInputMethodService {
- private static Trime self = null;
- private LiquidKeyboard liquidKeyboard;
- private boolean normalTextEditor;
-
- @NonNull
- private AppPrefs getPrefs() {
- return AppPrefs.defaultInstance();
- }
-
- private boolean darkMode; // 当前键盘主题是否处于暗黑模式
- private KeyboardView mainKeyboardView; // 主軟鍵盤
-
- private Candidate mCandidate; // 候選
- private Composition mComposition; // 編碼
- private CompositionRootBinding compositionRootBinding = null;
- private ScrollView mCandidateRoot, mTabRoot;
- private TabView tabView;
-
- @Nullable public InputView inputView = null;
- public WeakHashSet eventListeners = new WeakHashSet<>();
- public InputFeedbackManager inputFeedbackManager = null; // 效果管理器
- private IntentReceiver mIntentReceiver = null;
-
- public EditorInfo editorInfo = null;
-
- private boolean isWindowShown = false; // 键盘窗口是否已显示
-
- private boolean isAutoCaps; // 句首自動大寫
-
- public EditorInstance activeEditorInstance;
- public TextInputManager textInputManager; // 文字输入管理器
-
- private int minPopupSize; // 上悬浮窗的候选词的最小词长
- private int minPopupCheckSize; // 第一屏候选词数量少于设定值,则候选词上悬浮窗。(也就是说,第一屏存在长词)此选项大于1时,min_length等参数失效
- private CompositionPopupWindow mCompositionPopupWindow;
-
- public static Trime getService() {
- return self;
- }
-
- @Nullable
- public static Trime getServiceOrNull() {
- return self;
- }
-
- private static final Handler syncBackgroundHandler =
- new Handler(
- Looper.getMainLooper(),
- msg -> {
- if (!((Trime) msg.obj).isShowInputRequested()) { // 若当前没有输入面板,则后台同步。防止面板关闭后5秒内再次打开
- ShortcutUtils.INSTANCE.syncInBackground();
- ((Trime) msg.obj).loadConfig();
- }
- return false;
- });
-
- public Trime() {
- try {
- self = this;
- } catch (Exception e) {
- Timber.e(e);
- }
- }
-
- @Override
- public void onWindowShown() {
- super.onWindowShown();
- if (isWindowShown) {
- Timber.i("Ignoring (is already shown)");
- return;
- } else {
- Timber.i("onWindowShown...");
- }
-
- if (RimeWrapper.isReady() && activeEditorInstance != null) {
- isWindowShown = true;
- updateComposing();
-
- for (EventListener listener : eventListeners) {
- listener.onWindowShown();
- }
- }
- }
-
- @Override
- public void onWindowHidden() {
- super.onWindowHidden();
- if (!isWindowShown) {
- Timber.d("Ignoring (window is already hidden)");
- return;
- } else {
- Timber.d("onWindowHidden");
- }
- isWindowShown = false;
-
- if (getPrefs().getProfile().getSyncBackgroundEnabled()) {
- final Message msg = new Message();
- msg.obj = this;
- syncBackgroundHandler.sendMessageDelayed(msg, 5000); // 输入面板隐藏5秒后,开始后台同步
- }
-
- for (EventListener listener : eventListeners) {
- listener.onWindowHidden();
- }
- }
-
- public void updatePopupWindow(final int offsetX, final int offsetY) {
- mCompositionPopupWindow.updatePopupWindow(offsetX, offsetY);
- }
-
- public void loadConfig() {
- final Theme theme = Theme.get();
- minPopupSize = theme.style.getInt("layout/min_length");
- minPopupCheckSize = theme.style.getInt("layout/min_check");
- textInputManager.setShouldResetAsciiMode(theme.style.getBoolean("reset_ascii_mode"));
- isAutoCaps = theme.style.getBoolean("auto_caps");
- textInputManager.setShouldUpdateRimeOption(true);
-
- mCompositionPopupWindow.loadConfig(theme, getPrefs());
- }
-
- @SuppressWarnings("UnusedReturnValue")
- private boolean updateRimeOption() {
- try {
- if (textInputManager.getShouldUpdateRimeOption()) {
- Rime.setOption("soft_cursor", getPrefs().getKeyboard().getSoftCursorEnabled()); // 軟光標
- Rime.setOption("_horizontal", Theme.get().style.getBoolean("horizontal")); // 水平模式
- textInputManager.setShouldUpdateRimeOption(false);
- }
- } catch (Exception e) {
- e.printStackTrace();
- return false;
- }
- return true;
- }
-
- public void restartSystemStartTimingSync() { // 防止重启系统 强行停止应用时alarm任务丢失
- if (getPrefs().getProfile().getTimingSyncEnabled()) {
- long triggerTime = getPrefs().getProfile().getTimingSyncTriggerTime();
- AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
- PendingIntent pendingIntent =
- PendingIntent.getBroadcast( // 设置待发送的同步事件
- this,
- 0,
- new Intent("com.osfans.trime.timing.sync"),
- VERSION.SDK_INT >= VERSION_CODES.M
- ? (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)
- : PendingIntent.FLAG_UPDATE_CURRENT);
- if (VERSION.SDK_INT >= VERSION_CODES.M) { // 根据SDK设置alarm任务
- alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
- } else {
- alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
- }
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- // MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops
- try {
- // Additional try..catch wrapper as the event listeners chain or the super.onCreate() method
- // could crash
- // and lead to a crash loop
- Timber.d("onCreate");
- final InputMethodService context = this;
- RimeWrapper.INSTANCE.startup(
- () -> {
- Timber.d("Running Trime.onCreate");
- textInputManager =
- TextInputManager.Companion.getInstance(UiUtil.INSTANCE.isDarkMode(context));
- activeEditorInstance = new EditorInstance(context);
- inputFeedbackManager = new InputFeedbackManager(context);
- liquidKeyboard = new LiquidKeyboard(context);
- mCompositionPopupWindow = new CompositionPopupWindow();
- restartSystemStartTimingSync();
-
- try {
- for (EventListener listener : eventListeners) {
- listener.onCreate();
- }
- } catch (Exception e) {
- Timber.e(e);
- }
- Timber.d("Trime.onCreate completed");
- });
- } catch (Exception e) {
- Timber.e(e);
- }
- }
-
- /**
- * 变更配置的暗黑模式开关
- *
- * @param darkMode 设置为暗黑模式
- * @return 模式实际上是否有发生变更
- */
- public boolean setDarkMode(boolean darkMode) {
- if (darkMode != this.darkMode) {
- Timber.d("Dark mode changed: %s", darkMode);
- this.darkMode = darkMode;
- return true;
- }
- Timber.d("Dark mode not changed: %s", darkMode);
- return false;
- }
-
- private SymbolKeyboardType symbolKeyboardType = SymbolKeyboardType.NO_KEY;
-
- public void inputSymbol(final String text) {
- textInputManager.onPress(KeyEvent.KEYCODE_UNKNOWN);
- if (Rime.isAsciiMode()) Rime.setOption("ascii_mode", false);
- boolean asciiPunch = Rime.isAsciiPunch();
- if (asciiPunch) Rime.setOption("ascii_punct", false);
- textInputManager.onText("{Escape}" + text);
- if (asciiPunch) Rime.setOption("ascii_punct", true);
- Trime.getService().selectLiquidKeyboard(-1);
- }
-
- public void selectLiquidKeyboard(final int tabIndex) {
- if (inputView == null) return;
- if (tabIndex >= 0) {
- inputView.switchUiByState(KeyboardWindow.State.Symbol);
-
- symbolKeyboardType = liquidKeyboard.select(tabIndex);
- tabView.updateTabWidth();
-
- mTabRoot.setBackground(mCandidateRoot.getBackground());
- mTabRoot.move(tabView.getHightlightLeft(), tabView.getHightlightRight());
- showLiquidKeyboardToolbar();
- } else {
- symbolKeyboardType = SymbolKeyboardType.NO_KEY;
- // 设置液体键盘处于隐藏状态
- TabManager.get().setTabExited();
- inputView.switchUiByState(KeyboardWindow.State.Main);
- updateComposing();
- }
- }
-
- // 按键需要通过tab name来打开liquidKeyboard的指定tab
- public void selectLiquidKeyboard(@NonNull String name) {
- if (name.matches("-?\\d+")) selectLiquidKeyboard(Integer.parseInt(name));
- else if (name.matches("[A-Z]+")) selectLiquidKeyboard(SymbolKeyboardType.valueOf(name));
- else selectLiquidKeyboard(TabManager.getTagIndex(name));
- }
-
- public void selectLiquidKeyboard(SymbolKeyboardType type) {
- selectLiquidKeyboard(TabManager.getTagIndex(type));
- }
-
- public void pasteByChar() {
- commitTextByChar(Objects.requireNonNull(ShortcutUtils.pasteFromClipboard(this)).toString());
- }
-
- public void invalidate() {
- Rime.getInstance(false);
- Theme.get().destroy();
- reset();
- textInputManager.setShouldUpdateRimeOption(true);
- }
-
- private void showCompositionView(boolean isCandidate) {
- if (TextUtils.isEmpty(Rime.getCompositionText()) && isCandidate) {
- mCompositionPopupWindow.hideCompositionView();
- return;
- }
- compositionRootBinding.compositionRoot.measure(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- mCompositionPopupWindow.updateCompositionView(
- compositionRootBinding.compositionRoot.getMeasuredWidth(),
- compositionRootBinding.compositionRoot.getMeasuredHeight());
- }
-
- public void loadBackground() {
- final Theme theme = Theme.get();
- final int orientation = getResources().getConfiguration().orientation;
-
- final Drawable textBackground =
- theme.colors.getDrawable(
- "text_back_color",
- "layout/border",
- "border_color",
- "layout/round_corner",
- "layout/alpha");
- mCompositionPopupWindow.setThemeStyle(
- (int) DimensionsKt.dp2px(theme.style.getFloat("layout/elevation")), textBackground);
-
- if (mCandidateRoot != null) {
- final Drawable candidateBackground =
- theme.colors.getDrawable(
- "candidate_background",
- "candidate_border",
- "candidate_border_color",
- "candidate_border_round",
- null);
- if (candidateBackground != null) mCandidateRoot.setBackground(candidateBackground);
- }
-
- if (inputView == null) return;
-
- // 单手键盘模式
- int oneHandMode = 0;
- int[] padding =
- theme.getKeyboardPadding(oneHandMode, orientation == Configuration.ORIENTATION_LANDSCAPE);
- Timber.i(
- "update KeyboardPadding: Trime.loadBackground, padding= %s %s %s, orientation=%s",
- padding[0], padding[1], padding[2], orientation);
- mainKeyboardView.setPadding(padding[0], 0, padding[1], padding[2]);
-
- final Drawable inputRootBackground = theme.colors.getDrawable("root_background");
- if (inputRootBackground != null) {
- inputView.getKeyboardView().setBackground(inputRootBackground);
- } else {
- // 避免因为键盘整体透明而造成的异常
- inputView.getKeyboardView().setBackgroundColor(Color.BLACK);
- }
-
- tabView.reset();
- }
-
- public void resetKeyboard() {
- if (mainKeyboardView != null) {
- mainKeyboardView.setShowHint(!Rime.getOption("_hide_key_hint"));
- mainKeyboardView.setShowSymbol(!Rime.getOption("_hide_key_symbol"));
- mainKeyboardView.reset(); // 實體鍵盤無軟鍵盤
- }
- }
-
- public void resetCandidate() {
- if (mCandidateRoot != null) {
- loadBackground();
- setShowComment(!Rime.getOption("_hide_comment"));
- mCandidateRoot.setVisibility(!Rime.getOption("_hide_candidate") ? View.VISIBLE : View.GONE);
- mCandidate.reset();
- mCompositionPopupWindow.setPopupWindowEnabled(
- getPrefs().getKeyboard().getPopupWindowEnabled()
- && Theme.get().style.getObject("window") != null);
- mComposition.setVisibility(
- mCompositionPopupWindow.isPopupWindowEnabled() ? View.VISIBLE : View.GONE);
- mComposition.reset();
- }
- }
-
- /** 重置鍵盤、候選條、狀態欄等 !!注意,如果其中調用Rime.setOption,切換方案會卡住 */
- private void reset() {
- if (inputView == null) return;
- inputView.switchUiByState(KeyboardWindow.State.Main);
- loadConfig();
- updateDarkMode();
- final Theme theme = Theme.get(darkMode);
- theme.initCurrentColors(darkMode);
- SoundThemeManager.switchSound(theme.colors.getString("sound"));
- KeyboardSwitcher.newOrReset();
- resetCandidate();
- mCompositionPopupWindow.hideCompositionView();
- resetKeyboard();
- }
-
- /** Must be called on the UI thread */
- public void initKeyboard() {
- if (textInputManager != null) {
- reset();
- // setNavBarColor();
- textInputManager.setShouldUpdateRimeOption(true); // 不能在Rime.onMessage中調用set_option,會卡死
- bindKeyboardToInputView();
- // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground();
- updateComposing(); // 切換主題時刷新候選
- }
- }
-
- public void initKeyboardDarkMode(boolean darkMode) {
- final Theme theme = Theme.get();
- if (theme.getHasDarkLight()) {
- loadConfig();
- theme.initCurrentColors(darkMode);
- SoundThemeManager.switchSound(theme.colors.getString("sound"));
- KeyboardSwitcher.newOrReset();
- resetCandidate();
- mCompositionPopupWindow.hideCompositionView();
- resetKeyboard();
-
- // setNavBarColor();
- textInputManager.setShouldUpdateRimeOption(true); // 不能在Rime.onMessage中調用set_option,會卡死
- bindKeyboardToInputView();
- // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground();
- updateComposing(); // 切換主題時刷新候選
- }
- }
-
- @Override
- public void onDestroy() {
- if (mIntentReceiver != null) mIntentReceiver.unregisterReceiver(this);
- mIntentReceiver = null;
- if (inputFeedbackManager != null) inputFeedbackManager.destroy();
- inputFeedbackManager = null;
- inputView = null;
-
- for (EventListener listener : eventListeners) {
- listener.onDestroy();
- }
- eventListeners.clear();
- if (mCompositionPopupWindow != null) {
- mCompositionPopupWindow.destroy();
- }
- super.onDestroy();
-
- self = null;
- }
-
- private void handleReturnKey() {
- if (editorInfo == null) {
- sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER);
- return;
- }
- if ((editorInfo.inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_NULL) {
- sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER);
- return;
- }
- if (BitFlagsKt.hasFlag(editorInfo.imeOptions, EditorInfo.IME_FLAG_NO_ENTER_ACTION)) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) ic.commitText("\n", 1);
- return;
- }
- if (!TextUtils.isEmpty(editorInfo.actionLabel)
- && editorInfo.actionId != EditorInfo.IME_ACTION_UNSPECIFIED) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) ic.performEditorAction(editorInfo.actionId);
- return;
- }
- final int action = editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
- final InputConnection ic = getCurrentInputConnection();
- switch (action) {
- case EditorInfo.IME_ACTION_UNSPECIFIED:
- case EditorInfo.IME_ACTION_NONE:
- if (ic != null) ic.commitText("\n", 1);
- break;
- default:
- if (ic != null) ic.performEditorAction(action);
- break;
- }
- }
-
- @Override
- public void onConfigurationChanged(@NonNull Configuration newConfig) {
- final Configuration config = getResources().getConfiguration();
- if (config != null) {
- if (config.orientation != newConfig.orientation) {
- // Clear composing text and candidates for orientation change.
- performEscape();
- config.orientation = newConfig.orientation;
- }
- }
- super.onConfigurationChanged(newConfig);
- }
-
- @Override
- public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
- mCompositionPopupWindow.updateCursorAnchorInfo(cursorAnchorInfo);
- if (mCandidateRoot != null) {
- showCompositionView(true);
- }
- }
-
- @Override
- public void onUpdateSelection(
- int oldSelStart,
- int oldSelEnd,
- int newSelStart,
- int newSelEnd,
- int candidatesStart,
- int candidatesEnd) {
- super.onUpdateSelection(
- oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd);
- if ((candidatesEnd != -1) && ((newSelStart != candidatesEnd) || (newSelEnd != candidatesEnd))) {
- // 移動光標時,更新候選區
- if ((newSelEnd < candidatesEnd) && (newSelEnd >= candidatesStart)) {
- final int n = newSelEnd - candidatesStart;
- Rime.setCaretPos(n);
- updateComposing();
- }
- }
- if ((candidatesStart == -1 && candidatesEnd == -1) && (newSelStart == 0 && newSelEnd == 0)) {
- // 上屏後,清除候選區
- performEscape();
- }
- // Update the caps-lock status for the current cursor position.
- dispatchCapsStateToInputView();
- }
-
- @Override
- public void onComputeInsets(@NonNull InputMethodService.Insets outInsets) {
- int[] location = new int[] {0, 0};
- if (inputView != null) {
- inputView.getKeyboardView().getLocationInWindow(location);
- }
- int y = location[1];
- outInsets.contentTopInsets = y;
- outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_CONTENT;
- outInsets.touchableRegion.setEmpty();
- outInsets.visibleTopInsets = y;
- }
-
- @Override
- public View onCreateInputView() {
- Timber.d("onCreateInputView()");
- RimeWrapper.runAfterStarted(
- () -> {
- inputView = new InputView(this);
-
- mainKeyboardView = inputView.getKeyboardWindow().getOldMainInputView().mainKeyboardView;
- // 初始化候选栏
- mCandidateRoot = inputView.getQuickBar().getOldCandidateBar().getRoot();
- mCandidate = inputView.getQuickBar().getOldCandidateBar().candidates;
-
- // 候选词悬浮窗的容器
- compositionRootBinding = CompositionRootBinding.inflate(LayoutInflater.from(this));
- mComposition = compositionRootBinding.compositions;
- mCompositionPopupWindow.init(compositionRootBinding.compositionRoot, mCandidateRoot);
- mTabRoot = inputView.getQuickBar().getOldTabBar().getRoot();
-
- updateDarkMode();
- Theme.get(darkMode).initCurrentColors(darkMode);
-
- liquidKeyboard.setKeyboardView(
- inputView.getKeyboardWindow().getOldSymbolInputView().liquidKeyboardView);
- tabView = inputView.getQuickBar().getOldTabBar().tabs;
-
- for (EventListener listener : eventListeners) {
- listener.onInitializeInputUi(inputView);
- }
- loadBackground();
-
- KeyboardSwitcher.newOrReset();
- bindKeyboardToInputView();
-
- setInputView(inputView);
- Timber.d("onCreateInputView - completely ended");
- });
- Timber.d("onCreateInputView() finish");
-
- return new InitializationUi(this).getRoot();
- }
-
- @Override
- public void setInputView(View view) {
- final FrameLayout inputArea =
- Objects.requireNonNull(getWindow().getWindow())
- .getDecorView()
- .findViewById(android.R.id.inputArea);
- ViewGroup.LayoutParams lP1 = inputArea.getLayoutParams();
- lP1.height = ViewGroup.LayoutParams.MATCH_PARENT;
- inputArea.setLayoutParams(lP1);
- super.setInputView(view);
- ViewGroup.LayoutParams lP2 = view.getLayoutParams();
- lP2.height = ViewGroup.LayoutParams.MATCH_PARENT;
- view.setLayoutParams(lP2);
- }
-
- @Override
- public void onConfigureWindow(
- @NonNull Window win, boolean isFullscreen, boolean isCandidatesOnly) {
- win.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- }
-
- public void setShowComment(boolean show_comment) {
- if (mCandidateRoot != null) mCandidate.setShowComment(show_comment);
- mComposition.setShowComment(show_comment);
- }
-
- @Override
- public void onStartInput(EditorInfo attribute, boolean restarting) {
- editorInfo = attribute;
- Timber.d("onStartInput: restarting=%s", restarting);
- }
-
- private boolean updateDarkMode() {
- boolean isDarkMode = UiUtil.INSTANCE.isDarkMode(this);
-
- return setDarkMode(isDarkMode);
- }
-
- @Override
- public void onStartInputView(EditorInfo attribute, boolean restarting) {
- Timber.d("onStartInputView: restarting=%s", restarting);
- editorInfo = attribute;
-
- RimeWrapper.runAfterStarted(
- () -> {
- if (updateDarkMode()) {
- initKeyboardDarkMode(darkMode);
- }
-
- inputFeedbackManager.resumeSoundPool();
- inputFeedbackManager.resetPlayProgress();
- for (EventListener listener : eventListeners) {
- listener.onStartInputView(activeEditorInstance, restarting);
- }
- if (getPrefs().getOther().getShowStatusBarIcon()) {
- showStatusIcon(R.drawable.ic_trime_status); // 狀態欄圖標
- }
- bindKeyboardToInputView();
- // if (!restarting) setNavBarColor();
- setCandidatesViewShown(!Rime.isEmpty()); // 軟鍵盤出現時顯示候選欄
-
- if ((attribute.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION)
- == EditorInfo.IME_FLAG_NO_ENTER_ACTION) {
- mainKeyboardView.resetEnterLabel();
- } else {
- mainKeyboardView.setEnterLabel(
- attribute.imeOptions & EditorInfo.IME_MASK_ACTION, attribute.actionLabel);
- }
-
- switch (attribute.inputType & InputType.TYPE_MASK_VARIATION) {
- case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
- case InputType.TYPE_TEXT_VARIATION_PASSWORD:
- case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:
- case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
- case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD:
- Timber.i(
- "EditorInfo: private;"
- + " packageName="
- + attribute.packageName
- + "; fieldName="
- + attribute.fieldName
- + "; actionLabel="
- + attribute.actionLabel
- + "; inputType="
- + attribute.inputType
- + "; VARIATION="
- + (attribute.inputType & InputType.TYPE_MASK_VARIATION)
- + "; CLASS="
- + (attribute.inputType & InputType.TYPE_MASK_CLASS)
- + "; ACTION="
- + (attribute.imeOptions & EditorInfo.IME_MASK_ACTION));
- normalTextEditor = false;
- break;
-
- default:
- Timber.i(
- "EditorInfo: normal;"
- + " packageName="
- + attribute.packageName
- + "; fieldName="
- + attribute.fieldName
- + "; actionLabel="
- + attribute.actionLabel
- + "; inputType="
- + attribute.inputType
- + "; VARIATION="
- + (attribute.inputType & InputType.TYPE_MASK_VARIATION)
- + "; CLASS="
- + (attribute.inputType & InputType.TYPE_MASK_CLASS)
- + "; ACTION="
- + (attribute.imeOptions & EditorInfo.IME_MASK_ACTION));
-
- if ((attribute.imeOptions & EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING)
- == EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) {
- // 应用程求以隐身模式打开键盘应用程序
- normalTextEditor = false;
- Timber.d("EditorInfo: normal -> private, IME_FLAG_NO_PERSONALIZED_LEARNING");
- } else if (attribute.packageName.equals(BuildConfig.APPLICATION_ID)
- || getPrefs()
- .getClipboard()
- .getDraftExcludeApp()
- .contains(attribute.packageName)) {
- normalTextEditor = false;
- Timber.d("EditorInfo: normal -> exclude, packageName=%s", attribute.packageName);
- } else {
- normalTextEditor = true;
- DraftHelper.INSTANCE.onInputEventChanged();
- }
- }
- });
- RimeWrapper.INSTANCE.runCheck();
- }
-
- @Override
- public void onFinishInputView(boolean finishingInput) {
- super.onFinishInputView(finishingInput);
- if (RimeWrapper.isReady()) {
- if (normalTextEditor) {
- DraftHelper.INSTANCE.onInputEventChanged();
- }
- try {
- // Dismiss any pop-ups when the input-view is being finished and hidden.
- mainKeyboardView.closing();
- performEscape();
- if (inputFeedbackManager != null) {
- inputFeedbackManager.releaseSoundPool();
- }
- mCompositionPopupWindow.hideCompositionView();
- } catch (Exception e) {
- Timber.e(e, "Failed to show the PopupWindow.");
- }
- }
- if (inputView != null) {
- inputView.finishInput();
- }
- Timber.d("OnFinishInputView");
- }
-
- @Override
- public void onFinishInput() {
- editorInfo = null;
- super.onFinishInput();
- }
-
- public void bindKeyboardToInputView() {
- if (mainKeyboardView != null) {
- // Bind the selected keyboard to the input view.
- Keyboard sk = KeyboardSwitcher.getCurrentKeyboard();
- mainKeyboardView.setKeyboard(sk);
- dispatchCapsStateToInputView();
- }
- }
-
- /**
- * Dispatches cursor caps info to input view in order to implement auto caps lock at the start of
- * a sentence.
- */
- private void dispatchCapsStateToInputView() {
- if ((isAutoCaps && Rime.isAsciiMode())
- && (mainKeyboardView != null && !mainKeyboardView.isCapsOn())) {
- mainKeyboardView.setShifted(false, activeEditorInstance.getCursorCapsMode() != 0);
- }
- }
-
- private boolean isComposing() {
- return Rime.isComposing();
- }
-
- /**
- * Commit the current composing text together with the new text
- *
- * @param text the new text to be committed
- */
- public void commitText(String text) {
- getCurrentInputConnection().finishComposingText();
- activeEditorInstance.commitText(text, true);
- }
-
- public void commitTextByChar(String text) {
- for (int i = 0; i < text.length(); i++) {
- if (!activeEditorInstance.commitText(text.substring(i, i + 1), false)) break;
- }
- }
-
- /**
- * 如果爲{@link KeyEvent#KEYCODE_BACK Back鍵},則隱藏鍵盤
- *
- * @param keyCode {@link KeyEvent#getKeyCode() 鍵碼}
- * @return 是否處理了Back鍵事件
- */
- private boolean handleBack(int keyCode) {
- if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
- requestHideSelf(0);
- return true;
- }
- return false;
- }
-
- public boolean onRimeKey(int[] event) {
- updateRimeOption();
- // todo 改为异步处理按键事件、刷新UI
- final boolean ret = Rime.processKey(event[0], event[1]);
- activeEditorInstance.commitRimeText();
- return ret;
- }
-
- private boolean composeEvent(@NonNull KeyEvent event) {
- if (textInputManager == null) {
- return false;
- }
- final int keyCode = event.getKeyCode();
- if (keyCode == KeyEvent.KEYCODE_MENU) return false; // 不處理 Menu 鍵
- if (!Keycode.Companion.isStdKey(keyCode)) return false; // 只處理安卓標準按鍵
- if (event.getRepeatCount() == 0 && Key.isTrimeModifierKey(keyCode)) {
- boolean ret =
- onRimeKey(
- Event.getRimeEvent(
- keyCode,
- event.getAction() == KeyEvent.ACTION_DOWN
- ? event.getModifiers()
- : Rime.META_RELEASE_ON));
- if (isComposing()) setCandidatesViewShown(textInputManager.isComposable()); // 藍牙鍵盤打字時顯示候選欄
- return ret;
- }
- return textInputManager.isComposable() && !Rime.isVoidKeycode(keyCode);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- Timber.i("\t\tonKeyDown()\tkeycode=%d, event=%s", keyCode, event.toString());
- if (composeEvent(event) && onKeyEvent(event) && isWindowShown) return true;
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- Timber.i("\t\tonKeyUp()\tkeycode=%d, event=%s", keyCode, event.toString());
- if (composeEvent(event) && textInputManager.getNeedSendUpRimeKey()) {
- textInputManager.onRelease(keyCode);
- if (isWindowShown) return true;
- }
- return super.onKeyUp(keyCode, event);
- }
-
- /**
- * 處理實體鍵盤事件
- *
- * @param event {@link KeyEvent 按鍵事件}
- * @return 是否成功處理
- */
- // KeyEvent 处理实体键盘事件
- private boolean onKeyEvent(@NonNull KeyEvent event) {
- Timber.i("\t\tonKeyEvent()\tRealKeyboard event=%s", event.toString());
- int keyCode = event.getKeyCode();
- textInputManager.setNeedSendUpRimeKey(Rime.isComposing());
- if (!isComposing()) {
- if (keyCode == KeyEvent.KEYCODE_DEL
- || keyCode == KeyEvent.KEYCODE_ENTER
- || keyCode == KeyEvent.KEYCODE_ESCAPE
- || keyCode == KeyEvent.KEYCODE_BACK) {
- return false;
- }
- } else if (keyCode == KeyEvent.KEYCODE_BACK) {
- keyCode = KeyEvent.KEYCODE_ESCAPE; // 返回鍵清屏
- }
- if (event.getAction() == KeyEvent.ACTION_DOWN
- && event.isCtrlPressed()
- && event.getRepeatCount() == 0
- && !KeyEvent.isModifierKey(keyCode)) {
- if (hookKeyboard(keyCode, event.getMetaState())) return true;
- }
-
- final int unicodeChar = event.getUnicodeChar();
- final String s = String.valueOf((char) unicodeChar);
- final int i = Event.getClickCode(s);
- int mask = 0;
- if (i > 0) {
- keyCode = i;
- } else { // 空格、回車等
- mask = event.getMetaState();
- }
- final boolean ret = handleKey(keyCode, mask);
- if (isComposing()) setCandidatesViewShown(textInputManager.isComposable()); // 藍牙鍵盤打字時顯示候選欄
- return ret;
- }
-
- public void switchToPrevIme() {
- try {
- if (VERSION.SDK_INT >= VERSION_CODES.P) {
- switchToPreviousInputMethod();
- } else {
- Window window = getWindow().getWindow();
- if (window != null) {
- SystemServicesKt.getInputMethodManager()
- .switchToLastInputMethod(window.getAttributes().token);
- }
- }
- } catch (Exception e) {
- Timber.e(e, "Unable to switch to the previous IME.");
- SystemServicesKt.getInputMethodManager().showInputMethodPicker();
- }
- }
-
- public void switchToNextIme() {
- try {
- if (VERSION.SDK_INT >= VERSION_CODES.P) {
- switchToNextInputMethod(false);
- } else {
- Window window = getWindow().getWindow();
- if (window != null) {
- SystemServicesKt.getInputMethodManager()
- .switchToNextInputMethod(window.getAttributes().token, false);
- }
- }
- } catch (Exception e) {
- Timber.e(e, "Unable to switch to the next IME.");
- SystemServicesKt.getInputMethodManager().showInputMethodPicker();
- }
- }
-
- // 处理键盘事件(Android keycode)
- public boolean handleKey(int keyEventCode, int metaState) { // 軟鍵盤
- textInputManager.setNeedSendUpRimeKey(false);
- if (onRimeKey(Event.getRimeEvent(keyEventCode, metaState))) {
- // 如果输入法消费了按键事件,则需要释放按键
- textInputManager.setNeedSendUpRimeKey(true);
- Timber.d(
- "\t\thandleKey()\trimeProcess, keycode=%d, metaState=%d",
- keyEventCode, metaState);
- } else if (hookKeyboard(keyEventCode, metaState)) {
- Timber.d("\t\thandleKey()\thookKeyboard, keycode=%d", keyEventCode);
- } else if (performEnter(keyEventCode) || handleBack(keyEventCode)) {
- // 处理返回键(隐藏软键盘)和回车键(换行)
- // todo 确认是否有必要单独处理回车键?是否需要把back和escape全部占用?
- Timber.d("\t\thandleKey()\tEnterOrHide, keycode=%d", keyEventCode);
- } else if (ShortcutUtils.INSTANCE.openCategory(keyEventCode)) {
- // 打开系统默认应用
- Timber.d("\t\thandleKey()\topenCategory keycode=%d", keyEventCode);
- } else {
- textInputManager.setNeedSendUpRimeKey(true);
- Timber.d(
- "\t\thandleKey()\treturn FALSE, keycode=%d, metaState=%d",
- keyEventCode, metaState);
- return false;
- }
- return true;
- }
-
- public boolean shareText() {
- if (VERSION.SDK_INT >= VERSION_CODES.M) {
- final @Nullable InputConnection ic = getCurrentInputConnection();
- if (ic == null) return false;
- CharSequence cs = ic.getSelectedText(0);
- if (cs == null) ic.performContextMenuAction(android.R.id.selectAll);
- return ic.performContextMenuAction(android.R.id.shareText);
- }
- return false;
- }
-
- private boolean hookKeyboard(int code, int mask) { // 編輯操作
- final @Nullable InputConnection ic = getCurrentInputConnection();
- if (ic == null) return false;
- if (mask == KeyEvent.META_CTRL_ON) {
-
- if (VERSION.SDK_INT >= VERSION_CODES.M) {
- if (getPrefs().getKeyboard().getHookCtrlZY()) {
- switch (code) {
- case KeyEvent.KEYCODE_Y:
- return ic.performContextMenuAction(android.R.id.redo);
- case KeyEvent.KEYCODE_Z:
- return ic.performContextMenuAction(android.R.id.undo);
- }
- }
- }
- switch (code) {
- case KeyEvent.KEYCODE_A:
- if (getPrefs().getKeyboard().getHookCtrlA())
- return ic.performContextMenuAction(android.R.id.selectAll);
- return false;
- case KeyEvent.KEYCODE_X:
- if (getPrefs().getKeyboard().getHookCtrlCV()) {
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0;
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et != null) {
- if (et.selectionEnd - et.selectionStart > 0)
- return ic.performContextMenuAction(android.R.id.cut);
- }
- }
- Timber.i("hookKeyboard cut fail");
- return false;
- case KeyEvent.KEYCODE_C:
- if (getPrefs().getKeyboard().getHookCtrlCV()) {
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0;
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et != null) {
- if (et.selectionEnd - et.selectionStart > 0)
- return ic.performContextMenuAction(android.R.id.copy);
- }
- }
- Timber.i("hookKeyboard copy fail");
- return false;
- case KeyEvent.KEYCODE_V:
- if (getPrefs().getKeyboard().getHookCtrlCV()) {
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0;
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et == null) {
- Timber.d("hookKeyboard paste, et == null, try commitText");
- if (ic.commitText(ShortcutUtils.pasteFromClipboard(this), 1)) {
- return true;
- }
- } else if (ic.performContextMenuAction(android.R.id.paste)) {
- return true;
- }
- Timber.w("hookKeyboard paste fail");
- }
- return false;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- if (getPrefs().getKeyboard().getHookCtrlLR()) {
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0;
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et != null) {
- int move_to = StringUtils.findSectionAfter(et.text, et.startOffset + et.selectionEnd);
- ic.setSelection(move_to, move_to);
- return true;
- }
- break;
- }
- case KeyEvent.KEYCODE_DPAD_LEFT:
- if (getPrefs().getKeyboard().getHookCtrlLR()) {
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0;
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et != null) {
- int move_to =
- StringUtils.findSectionBefore(et.text, et.startOffset + et.selectionStart);
- ic.setSelection(move_to, move_to);
- return true;
- }
- break;
- }
- }
- }
- return false;
- }
-
- /** 更新Rime的中西文狀態、編輯區文本 */
- public int updateComposing() {
- final @Nullable InputConnection ic = getCurrentInputConnection();
- activeEditorInstance.updateComposingText();
- if (ic != null && !mCompositionPopupWindow.isWinFixed()) {
- mCompositionPopupWindow.setCursorUpdated(ic.requestCursorUpdates(1));
- }
- int startNum = 0;
- if (mCandidateRoot != null) {
- if (mCompositionPopupWindow.isPopupWindowEnabled()) {
- Timber.d("updateComposing() SymbolKeyboardType=%s", symbolKeyboardType.toString());
- if (symbolKeyboardType != SymbolKeyboardType.NO_KEY
- && symbolKeyboardType != SymbolKeyboardType.CANDIDATE) {
- showLiquidKeyboardToolbar();
- } else {
- mComposition.setVisibility(View.VISIBLE);
- startNum = mComposition.setWindow(minPopupSize, minPopupCheckSize, Integer.MAX_VALUE);
- mCandidate.setText(startNum);
- // if isCursorUpdated, showCompositionView will be called in onUpdateCursorAnchorInfo
- // otherwise we need to call it here
- if (!mCompositionPopupWindow.isCursorUpdated()) showCompositionView(true);
- }
- } else {
- mCandidate.setText(0);
- }
- mCandidate.setExpectWidth(mainKeyboardView.getWidth());
- // 刷新候选词后,如果候选词超出屏幕宽度,滚动候选栏
- mTabRoot.move(mCandidate.getHighlightLeft(), mCandidate.getHighlightRight());
- }
- if (mainKeyboardView != null) mainKeyboardView.invalidateComposingKeys();
- if (!onEvaluateInputViewShown())
- setCandidatesViewShown(textInputManager.isComposable()); // 實體鍵盤打字時顯示候選欄
-
- return startNum;
- }
-
- private void showLiquidKeyboardToolbar() {
- mComposition.changeToLiquidKeyboardToolbar();
- showCompositionView(false);
- }
-
- /**
- * 如果爲{@link KeyEvent#KEYCODE_ENTER 回車鍵},則換行
- *
- * @param keyCode {@link KeyEvent#getKeyCode() 鍵碼}
- * @return 是否處理了回車事件
- */
- private boolean performEnter(int keyCode) { // 回車
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- DraftHelper.INSTANCE.onInputEventChanged();
- handleReturnKey();
- return true;
- }
- return false;
- }
-
- /** 模擬PC鍵盤中Esc鍵的功能:清除輸入的編碼和候選項 */
- public void performEscape() {
- if (isComposing()) textInputManager.onKey(KeyEvent.KEYCODE_ESCAPE, 0);
- }
-
- @Override
- public boolean onEvaluateFullscreenMode() {
- final Configuration config = getResources().getConfiguration();
- if (config != null) {
- if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
- return false;
- } else {
- switch (getPrefs().getKeyboard().getFullscreenMode()) {
- case AUTO_SHOW:
- Timber.d("FullScreen: Auto");
- final EditorInfo ei = getCurrentInputEditorInfo();
- if (ei != null && (ei.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0) {
- return false;
- }
- case ALWAYS_SHOW:
- Timber.d("FullScreen: Always");
- return true;
- case NEVER_SHOW:
- Timber.d("FullScreen: Never");
- return false;
- }
- }
- }
- return false;
- }
-
- @Override
- public void updateFullscreenMode() {
- super.updateFullscreenMode();
- updateSoftInputWindowLayoutParameters();
- }
-
- /** Updates the layout params of the window and input view. */
- private void updateSoftInputWindowLayoutParameters() {
- final Window w = getWindow().getWindow();
- if (w == null) return;
- if (inputView != null) {
- final int layoutHeight =
- isFullscreenMode()
- ? WindowManager.LayoutParams.WRAP_CONTENT
- : WindowManager.LayoutParams.MATCH_PARENT;
- final View inputArea = w.findViewById(android.R.id.inputArea);
- // TODO: 需要获取到文本编辑框、完成按钮,设置其色彩和尺寸。
- // if (isFullscreenMode()) {
- // Timber.d("isFullscreenMode");
- // /* In Fullscreen mode, when layout contains transparent color,
- // * the background under input area will disturb users' typing,
- // * so set the input area as light pink */
- // inputArea.setBackgroundColor(parseColor("#ff660000"));
- // } else {
- // Timber.d("NotFullscreenMode");
- // /* Otherwise, set it as light gray to avoid potential issue */
- // inputArea.setBackgroundColor(parseColor("#dddddddd"));
- // }
-
- ViewUtils.updateLayoutHeightOf(inputArea, layoutHeight);
- ViewUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM);
- ViewUtils.updateLayoutHeightOf(inputView, layoutHeight);
- }
- }
-
- public boolean addEventListener(@NonNull EventListener listener) {
- return eventListeners.add(listener);
- }
-
- public boolean removeEventListener(@NonNull EventListener listener) {
- return eventListeners.remove(listener);
- }
-
- public interface EventListener {
- default void onCreate() {}
-
- default void onInitializeInputUi(@NonNull InputView inputView) {}
-
- default void onDestroy() {}
-
- default void onStartInputView(@NonNull EditorInstance instance, boolean restarting) {}
-
- default void osFinishInputView(boolean finishingInput) {}
-
- default void onWindowShown() {}
-
- default void onWindowHidden() {}
-
- default void onUpdateSelection() {}
- }
-
- private boolean candidateExPage = false;
-
- public boolean hasCandidateExPage() {
- return candidateExPage;
- }
-
- public void setCandidateExPage(boolean candidateExPage) {
- this.candidateExPage = candidateExPage;
- }
-}
diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.kt b/app/src/main/java/com/osfans/trime/ime/core/Trime.kt
new file mode 100644
index 0000000000..ec33f611a8
--- /dev/null
+++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.kt
@@ -0,0 +1,1283 @@
+/*
+ * Copyright (C) 2015-present, osfans
+ * waxaca@163.com https://github.com/osfans
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.osfans.trime.ime.core
+
+import android.annotation.SuppressLint
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Color
+import android.inputmethodservice.InputMethodService
+import android.os.Build
+import android.os.Build.VERSION_CODES
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.text.InputType
+import android.text.TextUtils
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import android.view.inputmethod.CursorAnchorInfo
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.ExtractedTextRequest
+import android.widget.FrameLayout
+import com.osfans.trime.BuildConfig
+import com.osfans.trime.R
+import com.osfans.trime.core.Rime
+import com.osfans.trime.core.Rime.Companion.compositionText
+import com.osfans.trime.core.Rime.Companion.getOption
+import com.osfans.trime.core.Rime.Companion.isAsciiMode
+import com.osfans.trime.core.Rime.Companion.isAsciiPunch
+import com.osfans.trime.core.Rime.Companion.isEmpty
+import com.osfans.trime.core.Rime.Companion.isVoidKeycode
+import com.osfans.trime.core.Rime.Companion.processKey
+import com.osfans.trime.core.Rime.Companion.setCaretPos
+import com.osfans.trime.core.Rime.Companion.setOption
+import com.osfans.trime.data.AppPrefs
+import com.osfans.trime.data.AppPrefs.Companion.defaultInstance
+import com.osfans.trime.data.db.DraftHelper.onInputEventChanged
+import com.osfans.trime.data.sound.SoundThemeManager.switchSound
+import com.osfans.trime.data.theme.Theme.Companion.get
+import com.osfans.trime.databinding.CompositionRootBinding
+import com.osfans.trime.ime.broadcast.IntentReceiver
+import com.osfans.trime.ime.core.RimeWrapper.isReady
+import com.osfans.trime.ime.core.RimeWrapper.runAfterStarted
+import com.osfans.trime.ime.core.RimeWrapper.runCheck
+import com.osfans.trime.ime.core.RimeWrapper.startup
+import com.osfans.trime.ime.enums.Keycode.Companion.isStdKey
+import com.osfans.trime.ime.enums.SymbolKeyboardType
+import com.osfans.trime.ime.keyboard.Event.Companion.getClickCode
+import com.osfans.trime.ime.keyboard.Event.Companion.getRimeEvent
+import com.osfans.trime.ime.keyboard.InitializationUi
+import com.osfans.trime.ime.keyboard.InputFeedbackManager
+import com.osfans.trime.ime.keyboard.Key.Companion.isTrimeModifierKey
+import com.osfans.trime.ime.keyboard.KeyboardSwitcher.currentKeyboard
+import com.osfans.trime.ime.keyboard.KeyboardSwitcher.newOrReset
+import com.osfans.trime.ime.keyboard.KeyboardView
+import com.osfans.trime.ime.keyboard.KeyboardWindow
+import com.osfans.trime.ime.landscapeinput.LandscapeInputUIMode
+import com.osfans.trime.ime.lifecycle.LifecycleInputMethodService
+import com.osfans.trime.ime.symbol.LiquidKeyboard
+import com.osfans.trime.ime.symbol.TabManager
+import com.osfans.trime.ime.symbol.TabView
+import com.osfans.trime.ime.text.Candidate
+import com.osfans.trime.ime.text.Composition
+import com.osfans.trime.ime.text.CompositionPopupWindow
+import com.osfans.trime.ime.text.ScrollView
+import com.osfans.trime.ime.text.TextInputManager
+import com.osfans.trime.ime.util.UiUtil.isDarkMode
+import com.osfans.trime.util.ShortcutUtils.openCategory
+import com.osfans.trime.util.ShortcutUtils.pasteFromClipboard
+import com.osfans.trime.util.ShortcutUtils.syncInBackground
+import com.osfans.trime.util.StringUtils.findSectionAfter
+import com.osfans.trime.util.StringUtils.findSectionBefore
+import com.osfans.trime.util.ViewUtils.updateLayoutGravityOf
+import com.osfans.trime.util.ViewUtils.updateLayoutHeightOf
+import com.osfans.trime.util.WeakHashSet
+import com.osfans.trime.util.dp2px
+import splitties.bitflags.hasFlag
+import splitties.systemservices.inputMethodManager
+import timber.log.Timber
+import java.util.Objects
+
+/** [輸入法][InputMethodService]主程序 */
+
+@Suppress("ktlint:standard:property-naming")
+open class Trime : LifecycleInputMethodService() {
+ private var liquidKeyboard: LiquidKeyboard? = null
+ private var normalTextEditor = false
+ private val prefs: AppPrefs
+ get() = defaultInstance()
+ private var darkMode = false // 当前键盘主题是否处于暗黑模式
+ private var mainKeyboardView: KeyboardView? = null // 主軟鍵盤
+ private var mCandidate: Candidate? = null // 候選
+ private var mComposition: Composition? = null // 編碼
+ private var compositionRootBinding: CompositionRootBinding? = null
+ private var mCandidateRoot: ScrollView? = null
+ private var mTabRoot: ScrollView? = null
+ private var tabView: TabView? = null
+ var inputView: InputView? = null
+ private var eventListeners = WeakHashSet()
+ var inputFeedbackManager: InputFeedbackManager? = null // 效果管理器
+ private var mIntentReceiver: IntentReceiver? = null
+ private var editorInfo: EditorInfo? = null
+ private var isWindowShown = false // 键盘窗口是否已显示
+ private var isAutoCaps = false // 句首自動大寫
+ var activeEditorInstance: EditorInstance? = null
+ var textInputManager: TextInputManager? = null // 文字输入管理器
+ private var minPopupSize = 0 // 上悬浮窗的候选词的最小词长
+ private var minPopupCheckSize = 0 // 第一屏候选词数量少于设定值,则候选词上悬浮窗。(也就是说,第一屏存在长词)此选项大于1时,min_length等参数失效
+ private var mCompositionPopupWindow: CompositionPopupWindow? = null
+ private var candidateExPage = false
+
+ fun hasCandidateExPage(): Boolean {
+ return candidateExPage
+ }
+
+ fun setCandidateExPage(value: Boolean) {
+ candidateExPage = value
+ }
+
+ init {
+ try {
+ check(self == null) { "Trime is already initialized" }
+ self = this
+ } catch (e: Exception) {
+ Timber.e(e)
+ }
+ }
+
+ override fun onWindowShown() {
+ super.onWindowShown()
+ if (isWindowShown) {
+ Timber.i("Ignoring (is already shown)")
+ return
+ } else {
+ Timber.i("onWindowShown...")
+ }
+ if (isReady() && activeEditorInstance != null) {
+ isWindowShown = true
+ updateComposing()
+ for (listener in eventListeners) {
+ listener.onWindowShown()
+ }
+ }
+ }
+
+ override fun onWindowHidden() {
+ super.onWindowHidden()
+ if (!isWindowShown) {
+ Timber.d("Ignoring (window is already hidden)")
+ return
+ } else {
+ Timber.d("onWindowHidden")
+ }
+ isWindowShown = false
+ if (prefs.profile.syncBackgroundEnabled) {
+ val msg = Message()
+ msg.obj = this
+ syncBackgroundHandler.sendMessageDelayed(msg, 5000) // 输入面板隐藏5秒后,开始后台同步
+ }
+ for (listener in eventListeners) {
+ listener.onWindowHidden()
+ }
+ }
+
+ fun updatePopupWindow(
+ offsetX: Int,
+ offsetY: Int,
+ ) {
+ mCompositionPopupWindow!!.updatePopupWindow(offsetX, offsetY)
+ }
+
+ fun loadConfig() {
+ val theme = get()
+ minPopupSize = theme.style.getInt("layout/min_length")
+ minPopupCheckSize = theme.style.getInt("layout/min_check")
+ textInputManager!!.shouldResetAsciiMode = theme.style.getBoolean("reset_ascii_mode")
+ isAutoCaps = theme.style.getBoolean("auto_caps")
+ textInputManager!!.shouldUpdateRimeOption = true
+ mCompositionPopupWindow!!.loadConfig(theme, prefs)
+ }
+
+ private fun updateRimeOption(): Boolean {
+ try {
+ if (textInputManager!!.shouldUpdateRimeOption) {
+ setOption("soft_cursor", prefs.keyboard.softCursorEnabled) // 軟光標
+ setOption("_horizontal", get().style.getBoolean("horizontal")) // 水平模式
+ textInputManager!!.shouldUpdateRimeOption = false
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /** 防止重启系统 强行停止应用时alarm任务丢失 */
+ @SuppressLint("ScheduleExactAlarm")
+ fun restartSystemStartTimingSync() {
+ if (prefs.profile.timingSyncEnabled) {
+ val triggerTime = prefs.profile.timingSyncTriggerTime
+ val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
+
+ /** 设置待发送的同步事件 */
+ val pendingIntent =
+ PendingIntent.getBroadcast(
+ this,
+ 0,
+ Intent("com.osfans.trime.timing.sync"),
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ },
+ )
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { // 根据SDK设置alarm任务
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
+ } else {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ // MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops
+ try {
+ // Additional try..catch wrapper as the event listeners chain or the super.onCreate() method
+ // could crash
+ // and lead to a crash loop
+ Timber.d("onCreate")
+ val context: InputMethodService = this
+ startup {
+ Timber.d("Running Trime.onCreate")
+ textInputManager = TextInputManager.getInstance(isDarkMode(context))
+ activeEditorInstance = EditorInstance(context)
+ inputFeedbackManager = InputFeedbackManager(context)
+ liquidKeyboard = LiquidKeyboard(context)
+ mCompositionPopupWindow = CompositionPopupWindow()
+ restartSystemStartTimingSync()
+ try {
+ for (listener in eventListeners) {
+ listener.onCreate()
+ }
+ } catch (e: Exception) {
+ Timber.e(e)
+ }
+ Timber.d("Trime.onCreate completed")
+ }
+ } catch (e: Exception) {
+ Timber.e(e)
+ }
+ }
+
+ /**
+ * 变更配置的暗黑模式开关
+ *
+ * @param darkMode 设置为暗黑模式
+ * @return 模式实际上是否有发生变更
+ */
+ private fun setDarkMode(darkMode: Boolean): Boolean {
+ if (darkMode != this.darkMode) {
+ Timber.d("Dark mode changed: %s", darkMode)
+ this.darkMode = darkMode
+ return true
+ }
+ Timber.d("Dark mode not changed: %s", darkMode)
+ return false
+ }
+
+ private var symbolKeyboardType = SymbolKeyboardType.NO_KEY
+
+ fun inputSymbol(text: String) {
+ textInputManager!!.onPress(KeyEvent.KEYCODE_UNKNOWN)
+ if (isAsciiMode) setOption("ascii_mode", false)
+ val asciiPunch = isAsciiPunch
+ if (asciiPunch) setOption("ascii_punct", false)
+ textInputManager!!.onText("{Escape}$text")
+ if (asciiPunch) setOption("ascii_punct", true)
+ self!!.selectLiquidKeyboard(-1)
+ }
+
+ fun selectLiquidKeyboard(tabIndex: Int) {
+ if (inputView == null) return
+ if (tabIndex >= 0) {
+ inputView!!.switchUiByState(KeyboardWindow.State.Symbol)
+ symbolKeyboardType = liquidKeyboard!!.select(tabIndex)
+ tabView!!.updateTabWidth()
+ mTabRoot!!.background = mCandidateRoot!!.background
+ mTabRoot!!.move(tabView!!.hightlightLeft, tabView!!.hightlightRight)
+ showLiquidKeyboardToolbar()
+ } else {
+ symbolKeyboardType = SymbolKeyboardType.NO_KEY
+ // 设置液体键盘处于隐藏状态
+ TabManager.get().setTabExited()
+ inputView!!.switchUiByState(KeyboardWindow.State.Main)
+ updateComposing()
+ }
+ }
+
+ // 按键需要通过tab name来打开liquidKeyboard的指定tab
+ fun selectLiquidKeyboard(name: String) {
+ if (name.matches("-?\\d+".toRegex())) {
+ selectLiquidKeyboard(name.toInt())
+ } else if (name.matches("[A-Z]+".toRegex())) {
+ selectLiquidKeyboard(SymbolKeyboardType.valueOf(name))
+ } else {
+ selectLiquidKeyboard(TabManager.getTagIndex(name))
+ }
+ }
+
+ fun selectLiquidKeyboard(type: SymbolKeyboardType?) {
+ selectLiquidKeyboard(TabManager.getTagIndex(type))
+ }
+
+ fun pasteByChar() {
+ commitTextByChar(Objects.requireNonNull(pasteFromClipboard(this)).toString())
+ }
+
+ fun invalidate() {
+ Rime.getInstance(false)
+ get().destroy()
+ reset()
+ textInputManager!!.shouldUpdateRimeOption = true
+ }
+
+ private fun showCompositionView(isCandidate: Boolean) {
+ if (TextUtils.isEmpty(compositionText) && isCandidate) {
+ mCompositionPopupWindow!!.hideCompositionView()
+ return
+ }
+ compositionRootBinding!!.compositionRoot.measure(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ )
+ mCompositionPopupWindow!!.updateCompositionView(
+ compositionRootBinding!!.compositionRoot.measuredWidth,
+ compositionRootBinding!!.compositionRoot.measuredHeight,
+ )
+ }
+
+ private fun loadBackground() {
+ val theme = get()
+ val orientation = resources.configuration.orientation
+ val textBackground =
+ theme.colors.getDrawable(
+ "text_back_color",
+ "layout/border",
+ "border_color",
+ "layout/round_corner",
+ "layout/alpha",
+ )
+ mCompositionPopupWindow!!.setThemeStyle(
+ dp2px(theme.style.getFloat("layout/elevation")).toInt().toFloat(),
+ textBackground,
+ )
+ if (mCandidateRoot != null) {
+ val candidateBackground =
+ theme.colors.getDrawable(
+ "candidate_background",
+ "candidate_border",
+ "candidate_border_color",
+ "candidate_border_round",
+ null,
+ )
+ if (candidateBackground != null) mCandidateRoot!!.background = candidateBackground
+ }
+ if (inputView == null) return
+
+ // 单手键盘模式
+ val oneHandMode = 0
+ val padding = theme.getKeyboardPadding(oneHandMode, orientation == Configuration.ORIENTATION_LANDSCAPE)
+ Timber.i(
+ "update KeyboardPadding: Trime.loadBackground, padding= %s %s %s, orientation=%s",
+ padding[0],
+ padding[1],
+ padding[2],
+ orientation,
+ )
+ mainKeyboardView!!.setPadding(padding[0], 0, padding[1], padding[2])
+ val inputRootBackground = theme.colors.getDrawable("root_background")
+ if (inputRootBackground != null) {
+ inputView!!.keyboardView.background = inputRootBackground
+ } else {
+ // 避免因为键盘整体透明而造成的异常
+ inputView!!.keyboardView.setBackgroundColor(Color.BLACK)
+ }
+ tabView!!.reset()
+ }
+
+ fun resetKeyboard() {
+ if (mainKeyboardView != null) {
+ mainKeyboardView!!.setShowHint(!getOption("_hide_key_hint"))
+ mainKeyboardView!!.setShowSymbol(!getOption("_hide_key_symbol"))
+ mainKeyboardView!!.reset() // 實體鍵盤無軟鍵盤
+ }
+ }
+
+ fun resetCandidate() {
+ if (mCandidateRoot != null) {
+ loadBackground()
+ setShowComment(!getOption("_hide_comment"))
+ mCandidateRoot!!.visibility = if (!getOption("_hide_candidate")) View.VISIBLE else View.GONE
+ mCandidate!!.reset()
+ mCompositionPopupWindow!!.isPopupWindowEnabled = (
+ prefs.keyboard.popupWindowEnabled &&
+ get().style.getObject("window") != null
+ )
+ mComposition!!.visibility = if (mCompositionPopupWindow!!.isPopupWindowEnabled) View.VISIBLE else View.GONE
+ mComposition!!.reset()
+ }
+ }
+
+ /** 重置鍵盤、候選條、狀態欄等 !!注意,如果其中調用Rime.setOption,切換方案會卡住 */
+ private fun reset() {
+ if (inputView == null) return
+ inputView!!.switchUiByState(KeyboardWindow.State.Main)
+ loadConfig()
+ updateDarkMode()
+ val theme = get(darkMode)
+ theme.initCurrentColors(darkMode)
+ switchSound(theme.colors.getString("sound"))
+ newOrReset()
+ resetCandidate()
+ mCompositionPopupWindow!!.hideCompositionView()
+ resetKeyboard()
+ }
+
+ /** Must be called on the UI thread */
+ fun initKeyboard() {
+ if (textInputManager != null) {
+ reset()
+ // setNavBarColor();
+ textInputManager!!.shouldUpdateRimeOption = true // 不能在Rime.onMessage中調用set_option,會卡死
+ bindKeyboardToInputView()
+ // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground();
+ updateComposing() // 切換主題時刷新候選
+ }
+ }
+
+ private fun initKeyboardDarkMode(darkMode: Boolean) {
+ val theme = get()
+ if (theme.hasDarkLight) {
+ loadConfig()
+ theme.initCurrentColors(darkMode)
+ switchSound(theme.colors.getString("sound"))
+ newOrReset()
+ resetCandidate()
+ mCompositionPopupWindow!!.hideCompositionView()
+ resetKeyboard()
+
+ // setNavBarColor();
+ textInputManager!!.shouldUpdateRimeOption = true // 不能在Rime.onMessage中調用set_option,會卡死
+ bindKeyboardToInputView()
+ // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground();
+ updateComposing() // 切換主題時刷新候選
+ }
+ }
+
+ override fun onDestroy() {
+ if (mIntentReceiver != null) mIntentReceiver!!.unregisterReceiver(this)
+ mIntentReceiver = null
+ if (inputFeedbackManager != null) inputFeedbackManager!!.destroy()
+ inputFeedbackManager = null
+ inputView = null
+ for (listener in eventListeners) {
+ listener.onDestroy()
+ }
+ eventListeners.clear()
+ if (mCompositionPopupWindow != null) {
+ mCompositionPopupWindow!!.destroy()
+ }
+ super.onDestroy()
+ self = null
+ }
+
+ private fun handleReturnKey() {
+ if (editorInfo == null) {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER)
+ return
+ }
+ if (editorInfo!!.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL) {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER)
+ return
+ }
+ if (editorInfo!!.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) {
+ val ic = currentInputConnection
+ ic?.commitText("\n", 1)
+ return
+ }
+ if (!TextUtils.isEmpty(editorInfo!!.actionLabel) &&
+ editorInfo!!.actionId != EditorInfo.IME_ACTION_UNSPECIFIED
+ ) {
+ val ic = currentInputConnection
+ ic?.performEditorAction(editorInfo!!.actionId)
+ return
+ }
+ val action = editorInfo!!.imeOptions and EditorInfo.IME_MASK_ACTION
+ val ic = currentInputConnection
+ when (action) {
+ EditorInfo.IME_ACTION_UNSPECIFIED, EditorInfo.IME_ACTION_NONE -> ic?.commitText("\n", 1)
+ else -> ic?.performEditorAction(action)
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ val config = resources.configuration
+ if (config != null) {
+ if (config.orientation != newConfig.orientation) {
+ // Clear composing text and candidates for orientation change.
+ performEscape()
+ config.orientation = newConfig.orientation
+ }
+ }
+ super.onConfigurationChanged(newConfig)
+ }
+
+ override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {
+ mCompositionPopupWindow!!.updateCursorAnchorInfo(cursorAnchorInfo)
+ if (mCandidateRoot != null) {
+ showCompositionView(true)
+ }
+ }
+
+ override fun onUpdateSelection(
+ oldSelStart: Int,
+ oldSelEnd: Int,
+ newSelStart: Int,
+ newSelEnd: Int,
+ candidatesStart: Int,
+ candidatesEnd: Int,
+ ) {
+ super.onUpdateSelection(
+ oldSelStart,
+ oldSelEnd,
+ newSelStart,
+ newSelEnd,
+ candidatesStart,
+ candidatesEnd,
+ )
+ if (candidatesEnd != -1 && (newSelStart != candidatesEnd || newSelEnd != candidatesEnd)) {
+ // 移動光標時,更新候選區
+ if (newSelEnd in candidatesStart..(android.R.id.inputArea)
+ val lP1 = inputArea.layoutParams
+ lP1.height = ViewGroup.LayoutParams.MATCH_PARENT
+ inputArea.layoutParams = lP1
+ super.setInputView(view)
+ val lP2 = view.layoutParams
+ lP2.height = ViewGroup.LayoutParams.MATCH_PARENT
+ view.layoutParams = lP2
+ }
+
+ override fun onConfigureWindow(
+ win: Window,
+ isFullscreen: Boolean,
+ isCandidatesOnly: Boolean,
+ ) {
+ win.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ }
+
+ fun setShowComment(showComment: Boolean) {
+ if (mCandidateRoot != null) mCandidate!!.setShowComment(showComment)
+ mComposition!!.setShowComment(showComment)
+ }
+
+ override fun onStartInput(
+ attribute: EditorInfo,
+ restarting: Boolean,
+ ) {
+ editorInfo = attribute
+ Timber.d("onStartInput: restarting=%s", restarting)
+ }
+
+ private fun updateDarkMode(): Boolean {
+ val isDarkMode = isDarkMode(this)
+ return setDarkMode(isDarkMode)
+ }
+
+ override fun onStartInputView(
+ attribute: EditorInfo,
+ restarting: Boolean,
+ ) {
+ Timber.d("onStartInputView: restarting=%s", restarting)
+ editorInfo = attribute
+ runAfterStarted {
+ if (updateDarkMode()) {
+ initKeyboardDarkMode(darkMode)
+ }
+ inputFeedbackManager!!.resumeSoundPool()
+ inputFeedbackManager!!.resetPlayProgress()
+ for (listener in eventListeners) {
+ listener.onStartInputView(activeEditorInstance!!, restarting)
+ }
+ if (prefs.other.showStatusBarIcon) {
+ showStatusIcon(R.drawable.ic_trime_status) // 狀態欄圖標
+ }
+ bindKeyboardToInputView()
+ // if (!restarting) setNavBarColor();
+ setCandidatesViewShown(!isEmpty) // 軟鍵盤出現時顯示候選欄
+ if (attribute.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION
+ == EditorInfo.IME_FLAG_NO_ENTER_ACTION
+ ) {
+ mainKeyboardView!!.resetEnterLabel()
+ } else {
+ mainKeyboardView!!.setEnterLabel(
+ attribute.imeOptions and EditorInfo.IME_MASK_ACTION,
+ attribute.actionLabel,
+ )
+ }
+ when (attribute.inputType and InputType.TYPE_MASK_VARIATION) {
+ InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
+ InputType.TYPE_TEXT_VARIATION_PASSWORD,
+ InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
+ InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS,
+ InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD,
+ -> {
+ Timber.i(
+ "EditorInfo: private;" +
+ " packageName=" +
+ attribute.packageName +
+ "; fieldName=" +
+ attribute.fieldName +
+ "; actionLabel=" +
+ attribute.actionLabel +
+ "; inputType=" +
+ attribute.inputType +
+ "; VARIATION=" +
+ (attribute.inputType and InputType.TYPE_MASK_VARIATION) +
+ "; CLASS=" +
+ (attribute.inputType and InputType.TYPE_MASK_CLASS) +
+ "; ACTION=" +
+ (attribute.imeOptions and EditorInfo.IME_MASK_ACTION),
+ )
+ normalTextEditor = false
+ }
+
+ else -> {
+ Timber.i(
+ "EditorInfo: normal;" +
+ " packageName=" +
+ attribute.packageName +
+ "; fieldName=" +
+ attribute.fieldName +
+ "; actionLabel=" +
+ attribute.actionLabel +
+ "; inputType=" +
+ attribute.inputType +
+ "; VARIATION=" +
+ (attribute.inputType and InputType.TYPE_MASK_VARIATION) +
+ "; CLASS=" +
+ (attribute.inputType and InputType.TYPE_MASK_CLASS) +
+ "; ACTION=" +
+ (attribute.imeOptions and EditorInfo.IME_MASK_ACTION),
+ )
+ if (attribute.imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+ == EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+ ) {
+ // 应用程求以隐身模式打开键盘应用程序
+ normalTextEditor = false
+ Timber.d("EditorInfo: normal -> private, IME_FLAG_NO_PERSONALIZED_LEARNING")
+ } else if (attribute.packageName == BuildConfig.APPLICATION_ID ||
+ prefs
+ .clipboard
+ .draftExcludeApp
+ .contains(attribute.packageName)
+ ) {
+ normalTextEditor = false
+ Timber.d("EditorInfo: normal -> exclude, packageName=%s", attribute.packageName)
+ } else {
+ normalTextEditor = true
+ onInputEventChanged()
+ }
+ }
+ }
+ }
+ runCheck()
+ }
+
+ override fun onFinishInputView(finishingInput: Boolean) {
+ super.onFinishInputView(finishingInput)
+ if (isReady()) {
+ if (normalTextEditor) {
+ onInputEventChanged()
+ }
+ try {
+ // Dismiss any pop-ups when the input-view is being finished and hidden.
+ mainKeyboardView!!.closing()
+ performEscape()
+ if (inputFeedbackManager != null) {
+ inputFeedbackManager!!.releaseSoundPool()
+ }
+ mCompositionPopupWindow!!.hideCompositionView()
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to show the PopupWindow.")
+ }
+ }
+ if (inputView != null) {
+ inputView!!.finishInput()
+ }
+ Timber.d("OnFinishInputView")
+ }
+
+ override fun onFinishInput() {
+ editorInfo = null
+ super.onFinishInput()
+ }
+
+ fun bindKeyboardToInputView() {
+ if (mainKeyboardView != null) {
+ // Bind the selected keyboard to the input view.
+ val sk = currentKeyboard
+ mainKeyboardView!!.keyboard = sk
+ dispatchCapsStateToInputView()
+ }
+ }
+
+ /**
+ * Dispatches cursor caps info to input view in order to implement auto caps lock at the start of
+ * a sentence.
+ */
+ private fun dispatchCapsStateToInputView() {
+ if (isAutoCaps && isAsciiMode && mainKeyboardView != null && !mainKeyboardView!!.isCapsOn) {
+ mainKeyboardView!!.setShifted(false, activeEditorInstance!!.cursorCapsMode != 0)
+ }
+ }
+
+ private val isComposing: Boolean
+ get() = Rime.isComposing
+
+ /**
+ * Commit the current composing text together with the new text
+ *
+ * @param text the new text to be committed
+ */
+ fun commitText(text: String?) {
+ currentInputConnection.finishComposingText()
+ activeEditorInstance!!.commitText(text!!, true)
+ }
+
+ private fun commitTextByChar(text: String) {
+ for (i in text.indices) {
+ if (!activeEditorInstance!!.commitText(text.substring(i, i + 1), false)) break
+ }
+ }
+
+ /**
+ * 如果爲Back鍵[KeyEvent.KEYCODE_BACK],則隱藏鍵盤
+ *
+ * @param keyCode 鍵碼[KeyEvent.getKeyCode]
+ * @return 是否處理了Back鍵事件
+ */
+ private fun handleBack(keyCode: Int): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ requestHideSelf(0)
+ return true
+ }
+ return false
+ }
+
+ private fun onRimeKey(event: IntArray): Boolean {
+ updateRimeOption()
+ // todo 改为异步处理按键事件、刷新UI
+ val ret = processKey(event[0], event[1])
+ activeEditorInstance!!.commitRimeText()
+ return ret
+ }
+
+ private fun composeEvent(event: KeyEvent): Boolean {
+ if (textInputManager == null) {
+ return false
+ }
+ val keyCode = event.keyCode
+ if (keyCode == KeyEvent.KEYCODE_MENU) return false // 不處理 Menu 鍵
+ if (!isStdKey(keyCode)) return false // 只處理安卓標準按鍵
+ if (event.repeatCount == 0 && isTrimeModifierKey(keyCode)) {
+ val ret =
+ onRimeKey(
+ getRimeEvent(
+ keyCode,
+ if (event.action == KeyEvent.ACTION_DOWN) event.modifiers else Rime.META_RELEASE_ON,
+ ),
+ )
+ if (this.isComposing) setCandidatesViewShown(textInputManager!!.isComposable) // 藍牙鍵盤打字時顯示候選欄
+ return ret
+ }
+ return textInputManager!!.isComposable && !isVoidKeycode(keyCode)
+ }
+
+ override fun onKeyDown(
+ keyCode: Int,
+ event: KeyEvent,
+ ): Boolean {
+ Timber.i("\t\tonKeyDown()\tkeycode=%d, event=%s", keyCode, event.toString())
+ return if (composeEvent(event) && onKeyEvent(event) && isWindowShown) true else super.onKeyDown(keyCode, event)
+ }
+
+ override fun onKeyUp(
+ keyCode: Int,
+ event: KeyEvent,
+ ): Boolean {
+ Timber.i("\t\tonKeyUp()\tkeycode=%d, event=%s", keyCode, event.toString())
+ if (composeEvent(event) && textInputManager!!.needSendUpRimeKey) {
+ textInputManager!!.onRelease(keyCode)
+ if (isWindowShown) return true
+ }
+ return super.onKeyUp(keyCode, event)
+ }
+
+ /**
+ * 处理实体键盘事件
+ *
+ * @param event 按鍵事件[KeyEvent]
+ * @return 是否成功處理
+ */
+ private fun onKeyEvent(event: KeyEvent): Boolean {
+ Timber.i("\t\tonKeyEvent()\tRealKeyboard event=%s", event.toString())
+ var keyCode = event.keyCode
+ textInputManager!!.needSendUpRimeKey = Rime.isComposing
+ if (!this.isComposing) {
+ if (keyCode == KeyEvent.KEYCODE_DEL ||
+ keyCode == KeyEvent.KEYCODE_ENTER ||
+ keyCode == KeyEvent.KEYCODE_ESCAPE ||
+ keyCode == KeyEvent.KEYCODE_BACK
+ ) {
+ return false
+ }
+ } else if (keyCode == KeyEvent.KEYCODE_BACK) {
+ keyCode = KeyEvent.KEYCODE_ESCAPE // 返回鍵清屏
+ }
+ if (event.action == KeyEvent.ACTION_DOWN && event.isCtrlPressed && event.repeatCount == 0 && !KeyEvent.isModifierKey(keyCode)) {
+ if (hookKeyboard(keyCode, event.metaState)) return true
+ }
+ val unicodeChar = event.unicodeChar
+ val s = unicodeChar.toChar().toString()
+ val i = getClickCode(s)
+ var mask = 0
+ if (i > 0) {
+ keyCode = i
+ } else { // 空格、回車等
+ mask = event.metaState
+ }
+ val ret = handleKey(keyCode, mask)
+ if (this.isComposing) setCandidatesViewShown(textInputManager!!.isComposable) // 藍牙鍵盤打字時顯示候選欄
+ return ret
+ }
+
+ fun switchToPrevIme() {
+ try {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
+ switchToPreviousInputMethod()
+ } else {
+ val window = window.window
+ if (window != null) {
+ inputMethodManager
+ .switchToLastInputMethod(window.attributes.token)
+ }
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Unable to switch to the previous IME.")
+ inputMethodManager.showInputMethodPicker()
+ }
+ }
+
+ fun switchToNextIme() {
+ try {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
+ switchToNextInputMethod(false)
+ } else {
+ val window = window.window
+ if (window != null) {
+ inputMethodManager
+ .switchToNextInputMethod(window.attributes.token, false)
+ }
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Unable to switch to the next IME.")
+ inputMethodManager.showInputMethodPicker()
+ }
+ }
+
+ // 处理键盘事件(Android keycode)
+ fun handleKey(
+ keyEventCode: Int,
+ metaState: Int,
+ ): Boolean { // 軟鍵盤
+ textInputManager!!.needSendUpRimeKey = false
+ if (onRimeKey(getRimeEvent(keyEventCode, metaState))) {
+ // 如果输入法消费了按键事件,则需要释放按键
+ textInputManager!!.needSendUpRimeKey = true
+ Timber.d(
+ "\t\thandleKey()\trimeProcess, keycode=%d, metaState=%d",
+ keyEventCode,
+ metaState,
+ )
+ } else if (hookKeyboard(keyEventCode, metaState)) {
+ Timber.d("\t\thandleKey()\thookKeyboard, keycode=%d", keyEventCode)
+ } else if (performEnter(keyEventCode) || handleBack(keyEventCode)) {
+ // 处理返回键(隐藏软键盘)和回车键(换行)
+ // todo 确认是否有必要单独处理回车键?是否需要把back和escape全部占用?
+ Timber.d("\t\thandleKey()\tEnterOrHide, keycode=%d", keyEventCode)
+ } else if (openCategory(keyEventCode)) {
+ // 打开系统默认应用
+ Timber.d("\t\thandleKey()\topenCategory keycode=%d", keyEventCode)
+ } else {
+ textInputManager!!.needSendUpRimeKey = true
+ Timber.d(
+ "\t\thandleKey()\treturn FALSE, keycode=%d, metaState=%d",
+ keyEventCode,
+ metaState,
+ )
+ return false
+ }
+ return true
+ }
+
+ fun shareText(): Boolean {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ val ic = currentInputConnection ?: return false
+ val cs = ic.getSelectedText(0)
+ if (cs == null) ic.performContextMenuAction(android.R.id.selectAll)
+ return ic.performContextMenuAction(android.R.id.shareText)
+ }
+ return false
+ }
+
+ /** 編輯操作 */
+ private fun hookKeyboard(
+ code: Int,
+ mask: Int,
+ ): Boolean {
+ val ic = currentInputConnection ?: return false
+ // 没按下 Ctrl 键
+ if (mask != KeyEvent.META_CTRL_ON) {
+ return false
+ }
+
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ if (prefs.keyboard.hookCtrlZY) {
+ when (code) {
+ KeyEvent.KEYCODE_Y -> return ic.performContextMenuAction(android.R.id.redo)
+ KeyEvent.KEYCODE_Z -> return ic.performContextMenuAction(android.R.id.undo)
+ }
+ }
+ }
+
+ when (code) {
+ KeyEvent.KEYCODE_A -> {
+ // 全选
+ return if (prefs.keyboard.hookCtrlA) ic.performContextMenuAction(android.R.id.selectAll) else false
+ }
+
+ KeyEvent.KEYCODE_X -> {
+ // 剪切
+ if (prefs.keyboard.hookCtrlCV) {
+ val etr = ExtractedTextRequest()
+ etr.token = 0
+ val et = ic.getExtractedText(etr, 0)
+ if (et != null) {
+ if (et.selectionEnd - et.selectionStart > 0) return ic.performContextMenuAction(android.R.id.cut)
+ }
+ }
+ Timber.i("hookKeyboard cut fail")
+ return false
+ }
+
+ KeyEvent.KEYCODE_C -> {
+ // 复制
+ if (prefs.keyboard.hookCtrlCV) {
+ val etr = ExtractedTextRequest()
+ etr.token = 0
+ val et = ic.getExtractedText(etr, 0)
+ if (et != null) {
+ if (et.selectionEnd - et.selectionStart > 0) return ic.performContextMenuAction(android.R.id.copy)
+ }
+ }
+ Timber.i("hookKeyboard copy fail")
+ return false
+ }
+
+ KeyEvent.KEYCODE_V -> {
+ // 粘贴
+ if (prefs.keyboard.hookCtrlCV) {
+ val etr = ExtractedTextRequest()
+ etr.token = 0
+ val et = ic.getExtractedText(etr, 0)
+ if (et == null) {
+ Timber.d("hookKeyboard paste, et == null, try commitText")
+ if (ic.commitText(pasteFromClipboard(this), 1)) {
+ return true
+ }
+ } else if (ic.performContextMenuAction(android.R.id.paste)) {
+ return true
+ }
+ Timber.w("hookKeyboard paste fail")
+ }
+ return false
+ }
+
+ KeyEvent.KEYCODE_DPAD_RIGHT -> {
+ if (prefs.keyboard.hookCtrlLR) {
+ val etr = ExtractedTextRequest()
+ etr.token = 0
+ val et = ic.getExtractedText(etr, 0)
+ if (et != null) {
+ val moveTo = findSectionAfter(et.text, et.startOffset + et.selectionEnd)
+ ic.setSelection(moveTo, moveTo)
+ return true
+ }
+ }
+ }
+
+ KeyEvent.KEYCODE_DPAD_LEFT ->
+ if (prefs.keyboard.hookCtrlLR) {
+ val etr = ExtractedTextRequest()
+ etr.token = 0
+ val et = ic.getExtractedText(etr, 0)
+ if (et != null) {
+ val moveTo = findSectionBefore(et.text, et.startOffset + et.selectionStart)
+ ic.setSelection(moveTo, moveTo)
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ /** 更新Rime的中西文狀態、編輯區文本 */
+ fun updateComposing(): Int {
+ val ic = currentInputConnection
+ activeEditorInstance!!.updateComposingText()
+ if (ic != null && !mCompositionPopupWindow!!.isWinFixed()) {
+ mCompositionPopupWindow!!.isCursorUpdated = ic.requestCursorUpdates(1)
+ }
+ var startNum = 0
+ if (mCandidateRoot != null) {
+ if (mCompositionPopupWindow!!.isPopupWindowEnabled) {
+ Timber.d("updateComposing() SymbolKeyboardType=%s", symbolKeyboardType.toString())
+ if (symbolKeyboardType != SymbolKeyboardType.NO_KEY &&
+ symbolKeyboardType != SymbolKeyboardType.CANDIDATE
+ ) {
+ showLiquidKeyboardToolbar()
+ } else {
+ mComposition!!.visibility = View.VISIBLE
+ startNum = mComposition!!.setWindow(minPopupSize, minPopupCheckSize, Int.MAX_VALUE)
+ mCandidate!!.setText(startNum)
+ // if isCursorUpdated, showCompositionView will be called in onUpdateCursorAnchorInfo
+ // otherwise we need to call it here
+ if (!mCompositionPopupWindow!!.isCursorUpdated) showCompositionView(true)
+ }
+ } else {
+ mCandidate!!.setText(0)
+ }
+ mCandidate!!.setExpectWidth(mainKeyboardView!!.width)
+ // 刷新候选词后,如果候选词超出屏幕宽度,滚动候选栏
+ mTabRoot!!.move(mCandidate!!.highlightLeft, mCandidate!!.highlightRight)
+ }
+ if (mainKeyboardView != null) mainKeyboardView!!.invalidateComposingKeys()
+ if (!onEvaluateInputViewShown()) setCandidatesViewShown(textInputManager!!.isComposable) // 實體鍵盤打字時顯示候選欄
+ return startNum
+ }
+
+ private fun showLiquidKeyboardToolbar() {
+ mComposition!!.changeToLiquidKeyboardToolbar()
+ showCompositionView(false)
+ }
+
+ /**
+ * 如果爲回車鍵[KeyEvent.KEYCODE_ENTER],則換行
+ *
+ * @param keyCode 鍵碼[KeyEvent.getKeyCode]
+ * @return 是否處理了回車事件
+ */
+ private fun performEnter(keyCode: Int): Boolean { // 回車
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ onInputEventChanged()
+ handleReturnKey()
+ return true
+ }
+ return false
+ }
+
+ /** 模擬PC鍵盤中Esc鍵的功能:清除輸入的編碼和候選項 */
+ fun performEscape() {
+ if (this.isComposing) textInputManager!!.onKey(KeyEvent.KEYCODE_ESCAPE, 0)
+ }
+
+ override fun onEvaluateFullscreenMode(): Boolean {
+ val config = resources.configuration
+ if (config == null || config.orientation != Configuration.ORIENTATION_LANDSCAPE) return false
+ return when (prefs.keyboard.fullscreenMode) {
+ LandscapeInputUIMode.AUTO_SHOW -> {
+ Timber.d("FullScreen: Auto")
+ val ei = currentInputEditorInfo
+ if (ei != null && ei.imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN != 0) {
+ return false
+ }
+ Timber.d("FullScreen: Always")
+ true
+ }
+
+ LandscapeInputUIMode.ALWAYS_SHOW -> {
+ Timber.d("FullScreen: Always")
+ true
+ }
+
+ LandscapeInputUIMode.NEVER_SHOW -> {
+ Timber.d("FullScreen: Never")
+ false
+ }
+ }
+ }
+
+ override fun updateFullscreenMode() {
+ super.updateFullscreenMode()
+ updateSoftInputWindowLayoutParameters()
+ }
+
+ /** Updates the layout params of the window and input view. */
+ private fun updateSoftInputWindowLayoutParameters() {
+ val w = window.window ?: return
+ if (inputView != null) {
+ val layoutHeight =
+ if (isFullscreenMode) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ val inputArea = w.findViewById(android.R.id.inputArea)
+ // TODO: 需要获取到文本编辑框、完成按钮,设置其色彩和尺寸。
+ // if (isFullscreenMode()) {
+ // Timber.d("isFullscreenMode");
+ // /* In Fullscreen mode, when layout contains transparent color,
+ // * the background under input area will disturb users' typing,
+ // * so set the input area as light pink */
+ // inputArea.setBackgroundColor(parseColor("#ff660000"));
+ // } else {
+ // Timber.d("NotFullscreenMode");
+ // /* Otherwise, set it as light gray to avoid potential issue */
+ // inputArea.setBackgroundColor(parseColor("#dddddddd"));
+ // }
+ updateLayoutHeightOf(inputArea, layoutHeight)
+ updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
+ updateLayoutHeightOf(inputView!!, layoutHeight)
+ }
+ }
+
+ fun addEventListener(listener: EventListener): Boolean {
+ return eventListeners.add(listener)
+ }
+
+ fun removeEventListener(listener: EventListener): Boolean {
+ return eventListeners.remove(listener)
+ }
+
+ interface EventListener {
+ fun onCreate() {}
+
+ fun onInitializeInputUi(inputView: InputView) {}
+
+ fun onDestroy() {}
+
+ fun onStartInputView(
+ instance: EditorInstance,
+ restarting: Boolean,
+ ) {}
+
+ fun osFinishInputView(finishingInput: Boolean) {}
+
+ fun onWindowShown() {}
+
+ fun onWindowHidden() {}
+
+ fun onUpdateSelection() {}
+ }
+
+ companion object {
+ var self: Trime? = null
+
+ @JvmStatic
+ fun getService(): Trime {
+ return self ?: throw IllegalStateException("Trime not initialized")
+ }
+
+ fun getServiceOrNull(): Trime? {
+ return self
+ }
+
+ private val syncBackgroundHandler =
+ Handler(
+ Looper.getMainLooper(),
+ ) { msg: Message ->
+ // 若当前没有输入面板,则后台同步。防止面板关闭后5秒内再次打开
+ if (!(msg.obj as Trime).isShowInputRequested) {
+ syncInBackground()
+ (msg.obj as Trime).loadConfig()
+ }
+ false
+ }
+ }
+}
diff --git a/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt b/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt
index 8392ffdc49..d8a7ea8ca7 100644
--- a/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt
+++ b/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt
@@ -59,7 +59,7 @@ class TextInputManager private constructor(private val isDarkMode: Boolean) :
private val trime get() = Trime.getService()
private val prefs get() = AppPrefs.defaultInstance()
private val activeEditorInstance: EditorInstance
- get() = trime.activeEditorInstance
+ get() = trime.activeEditorInstance as EditorInstance
private var intentReceiver: IntentReceiver? = null
private var rimeNotiHandlerJob: Job? = null
@@ -243,7 +243,7 @@ class TextInputManager private constructor(private val isDarkMode: Boolean) :
val value = notification.value
when (val option = notification.option) {
"ascii_mode" -> {
- trime.inputFeedbackManager.ttsLanguage =
+ trime.inputFeedbackManager?.ttsLanguage =
locales[if (value) 1 else 0]
}
"_hide_comment" -> trime.setShowComment(!value)