diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
deleted file mode 100644
index 2121e9d4d..000000000
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-name: 问题报告 / Bug Report
-about: 创建问题报告以帮助我们改进 / Create a bug report to help us improve
-title: ''
-labels: bug
-assignees: ''
-
----
-
-#### 摘要 / Summary
-
-
-#### 重现步骤 / Steps to Reproduce
-
-
-#### 预期行为 / Expected Behavior
-
-
-#### 日志 / Log
-
-
-#### 截图 / Screenshots
-
-
-#### 设备信息 / Device Infomation
-
-- 系统版本 / OS Version:
-- 应用版本 / App Version:
-- 插件版本 / Plugins Version:
-
-#### 附加信息 / Additional Context
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 000000000..9a79cd21b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,83 @@
+name: 问题报告 / Bug Report
+description: 创建问题报告以帮助我们改进 / Create a bug report to help us improve
+labels:
+ - bug
+
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: 摘要 / Summary
+ description: 简要描述遇到的问题。 / Briefly describe the bug.
+ validations:
+ required: true
+
+ - type: textarea
+ id: step_to_reproduce
+ attributes:
+ label: 重现步骤 / Steps to Reproduce
+ description: 如何重现该问题。 / How to reproduce the bug.
+ placeholder: |
+ 1. 打开某界面 / Open page ...
+ 2. 点击某菜单 / Click menu ...
+ 3. 某处出问题 / Something went wrong ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected_behavior
+ attributes:
+ label: 预期行为 / Expected Behavior
+ description: 完成上述步骤后应该发生什么。 / What is expected to happen after the steps above.
+ validations:
+ required: true
+
+ - type: textarea
+ id: log
+ attributes:
+ label: 日志 / Log
+ description: 附上日志以帮助定位问题。 / Attach log to help locate the bug.
+ validations:
+ required: false
+
+ - type: textarea
+ id: screenshot
+ attributes:
+ label: 截图 / Screenshot
+ description: 附上截图以帮助解释问题。 / Attach screenshots to help explain the bug.
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional_context
+ attributes:
+ label: 附加信息 / Additional Context
+ description: 与此问题相关的上下文信息,比如在问题出现前做了什么。 / Additional context about the bug, eg. what did you do before the bug appears.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ ### 设备信息 / Device Infomation
+
+ - type: input
+ id: os_version
+ attributes:
+ label: 系统版本 / OS Version
+ validations:
+ required: true
+
+ - type: input
+ id: app_version
+ attributes:
+ label: 应用版本 / App Version
+ validations:
+ required: true
+
+ - type: input
+ id: plugins_version
+ attributes:
+ label: 插件版本 / Plugins Version
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 99d680b0a..35299216a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,2 +1,14 @@
blank_issues_enabled: false
+contact_links:
+ - name: GitHub 讨论区 / GitHub Discussions
+ url: https://github.com/fcitx5-android/fcitx5-android/discussions
+ about: 请在这里提出有关如何使用本输入法的疑问。 / Please ask questions about how to use the input method here.
+
+ - name: Telegram 群组 / Telegram Group
+ url: https://t.me/fcitx5_android_group
+ about: 也可以群组中提问或讨论新功能。 / You may also ask questions or discuss new features in the group.
+
+ - name: Matrix 房间 / Matrix Room
+ url: https://matrix.to/#/#fcitx5-android:mozilla.org
+ about: Matrix 房间与 Telegram 群组通过桥接机器人互通。 / Matrix Room and Telegram Group are connected through bridge bot.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
deleted file mode 100644
index 580097f71..000000000
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-name: 功能请求 / Feature Request
-about: 为本项目提供新功能建议 / Suggest a new feature for this project
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-#### 摘要 / Summary
-
-
-#### 替代方案 / Alternative Solution
-
-
-#### 附加信息 / Additional Context
-
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 000000000..30f66b8bf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,55 @@
+name: 功能请求 / Feature Request
+description: 为本项目提供新功能建议 / Suggest a new feature for this project
+labels:
+ - enhancement
+
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: 摘要 / Summary
+ description: 新功能应该做什么。 / What the new feature should do.
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternative
+ attributes:
+ label: 替代方案 / Alternative Solution
+ description: 其它可能的解决方案(如果有)。 / Other possible solutions, if any.
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional_context
+ attributes:
+ label: 附加信息 / Additional Context
+ description: 与此功能请求有关的上下文信息或截图。 / Additional context or screenshots about the feature request.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ ### 设备信息 / Device Infomation
+
+ - type: input
+ id: os_version
+ attributes:
+ label: 系统版本 / OS Version
+ validations:
+ required: true
+
+ - type: input
+ id: app_version
+ attributes:
+ label: 应用版本 / App Version
+ validations:
+ required: true
+
+ - type: input
+ id: plugins_version
+ attributes:
+ label: 插件版本 / Plugins Version
+ validations:
+ required: false
diff --git a/.github/workflows/fdroid.yml b/.github/workflows/fdroid.yml
index ab4ac517f..8d612b32e 100644
--- a/.github/workflows/fdroid.yml
+++ b/.github/workflows/fdroid.yml
@@ -20,8 +20,8 @@ defaults:
jobs:
fdroid-build:
- runs-on: ubuntu-latest
- container: registry.gitlab.com/fdroid/fdroidserver:buildserver-bullseye
+ runs-on: ubuntu-24.04
+ container: registry.gitlab.com/fdroid/fdroidserver:buildserver-bookworm
strategy:
matrix:
abi:
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index fc833b212..01968ac76 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -7,27 +7,21 @@ on:
jobs:
develop:
- strategy:
- matrix:
- # Nix doesn't support Android toolchain on aarch64-darwin yet
- # https://github.com/NixOS/nixpkgs/issues/303968
-# os: [ubuntu-latest, macOS-latest]
- os: [ubuntu-latest]
- runs-on: ${{ matrix.os }}
+ runs-on: ubuntu-24.04
steps:
- name: Fetch source code
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- - uses: cachix/install-nix-action@v26
+ - uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: cachix/cachix-action@v14
+ - uses: cachix/cachix-action@v16
with:
name: fcitx5-android
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- - name: Build Debug APK
+ - name: Build Release APK
run: |
- nix develop .#noAS --command ./gradlew :app:assembleDebug
- nix develop .#noAS --command ./gradlew :assembleDebugPlugins
+ nix develop .#noAS --command ./gradlew :app:assembleRelease
+ nix develop .#noAS --command ./gradlew :assembleReleasePlugins
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 96f19566e..43438a178 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,10 +10,7 @@ on:
jobs:
publish:
- runs-on: ubuntu-22.04
- env:
- GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
- GITHUB_ACTOR: android-fcitx5
+ runs-on: ubuntu-24.04
steps:
- name: Fetch source code
uses: actions/checkout@v4
@@ -21,6 +18,11 @@ jobs:
fetch-depth: 0
submodules: recursive
+ - name: Install system dependencies
+ run: |
+ sudo apt update
+ sudo apt install extra-cmake-modules gettext
+
- name: Setup Java
uses: actions/setup-java@v4
with:
@@ -29,22 +31,17 @@ jobs:
- name: Setup Android environment
uses: android-actions/setup-android@v3
-
- - name: Install Android NDK
- run: |
- sdkmanager --install "cmake;3.22.1"
-
- - name: Install system dependencies
- run: |
- sudo apt update
- sudo apt install extra-cmake-modules gettext
+ with:
+ packages: cmake;3.31.6
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@v4
- name: Publish build convention and libs
+ env:
+ GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
+ GITHUB_ACTOR: fcitx5-android-bot
run: |
./gradlew :build-logic:convention:publish
./gradlew :lib:common:publish
./gradlew :lib:plugin-base:publish
-
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index a5318369b..dcc86c2e1 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -15,17 +15,10 @@ jobs:
fail-fast: false
matrix:
os:
- - ubuntu-22.04
+ - ubuntu-24.04
- macos-13
- macos-14
- windows-2022
- abi:
- - armeabi-v7a
- - arm64-v8a
- - x86
- - x86_64
- env:
- BUILD_ABI: ${{ matrix.abi }}
steps:
- name: Fetch source code
uses: actions/checkout@v4
@@ -33,12 +26,6 @@ jobs:
fetch-depth: 0
submodules: recursive
- - name: Regenerate symlinks pointing to submodule (Windows)
- if: ${{ matrix.os == 'windows-2022' }}
- run: |
- Remove-Item -Recurse app/src/main/assets/usr/share, plugin/hangul/src/main/assets/usr/share/libhangul, plugin/chewing/src/main/assets/usr/share/libchewing, plugin/jyutping/src/main/assets/usr/share/libime
- git checkout -- .
-
- name: Setup Java
uses: actions/setup-java@v4
with:
@@ -50,10 +37,10 @@ jobs:
- name: Install Android NDK
run: |
- sdkmanager --install "cmake;3.22.1"
+ sdkmanager --install "cmake;3.31.6"
- name: Install system dependencies (Ubuntu)
- if: ${{ matrix.os == 'ubuntu-22.04' }}
+ if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
sudo apt update
sudo apt install extra-cmake-modules gettext
@@ -64,25 +51,25 @@ jobs:
brew install extra-cmake-modules
- name: Install system dependencies (Windows)
- if: ${{ matrix.os == 'windows-2022' }}
+ if: ${{ startsWith(matrix.os, 'windows') }}
run: |
C:/msys64/usr/bin/pacman -Syu --noconfirm
C:/msys64/usr/bin/pacman -S --noconfirm mingw-w64-ucrt-x86_64-gettext mingw-w64-ucrt-x86_64-extra-cmake-modules
Add-Content $env:GITHUB_PATH "C:/msys64/ucrt64/bin"
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@v4
- - name: Build Debug APK
+ - name: Build Release APK
run: |
- ./gradlew :app:assembleDebug
- ./gradlew :assembleDebugPlugins
+ ./gradlew :app:assembleRelease
+ ./gradlew :assembleReleasePlugins
- name: Upload app
uses: actions/upload-artifact@v4
with:
- name: app-${{ matrix.os }}-${{ matrix.abi }}
- path: app/build/outputs/apk/debug/
+ name: app-${{ matrix.os }}
+ path: app/build/outputs/apk/release/
- name: Pack plugins
shell: bash
@@ -92,12 +79,12 @@ jobs:
do
if [ -d "plugin/${i}" ]
then
- mv "plugin/${i}/build/outputs/apk/debug" "plugins-to-upload/${i}"
+ mv "plugin/${i}/build/outputs/apk/release" "plugins-to-upload/${i}"
fi
done
- name: Upload plugins
uses: actions/upload-artifact@v4
with:
- name: plugins-${{ matrix.os }}-${{ matrix.abi }}
+ name: plugins-${{ matrix.os }}
path: plugins-to-upload
diff --git a/.gitignore b/.gitignore
index 7538326e1..0bd6d8615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,21 +1,12 @@
# Module :app
# Installed data
-/app/src/main/assets/usr/share/fcitx5/addon
-/app/src/main/assets/usr/share/fcitx5/chttrans
-/app/src/main/assets/usr/share/fcitx5/data
-/app/src/main/assets/usr/share/fcitx5/default
-/app/src/main/assets/usr/share/fcitx5/inputmethod
-/app/src/main/assets/usr/share/fcitx5/lua
-/app/src/main/assets/usr/share/fcitx5/punctuation
-/app/src/main/assets/usr/share/fcitx5/unicode
-/app/src/main/assets/usr/share/locale
+/app/src/main/assets/usr/
# Generated asset descriptor
/app/src/main/assets/descriptor.json
# Plugins
# Installed data
-/plugin/*/src/main/assets/usr/share/fcitx5
-/plugin/*/src/main/assets/usr/share/locale
+/plugin/*/src/main/assets/usr/
# Generated asset descriptor
/plugin/*/src/main/assets/descriptor.json
@@ -108,3 +99,6 @@ lint/tmp/
# Android Profiling
*.hprof
+
+### Kotlin ###
+.kotlin/
diff --git a/.gitmodules b/.gitmodules
index 67596fc97..2f3d38b6d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,54 +1,58 @@
[submodule "lib/fcitx5/src/main/cpp/fcitx5"]
path = lib/fcitx5/src/main/cpp/fcitx5
- url = git@github.com:fcitx/fcitx5.git
+ url = https://github.com/fcitx/fcitx5.git
[submodule "lib/fcitx5/src/main/cpp/prebuilt"]
path = lib/fcitx5/src/main/cpp/prebuilt
- url = git@github.com:fcitx5-android/prebuilt.git
+ url = https://github.com/fcitx5-android/prebuilt.git
+ shallow = true
[submodule "lib/fcitx5-lua/src/main/cpp/fcitx5-lua"]
path = lib/fcitx5-lua/src/main/cpp/fcitx5-lua
- url = git@github.com:fcitx/fcitx5-lua.git
+ url = https://github.com/fcitx/fcitx5-lua.git
[submodule "lib/libime/src/main/cpp/libime"]
path = lib/libime/src/main/cpp/libime
- url = git@github.com:fcitx/libime.git
+ url = https://github.com/fcitx/libime.git
[submodule "lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons"]
path = lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons
- url = git@github.com:fcitx/fcitx5-chinese-addons.git
+ url = https://github.com/fcitx/fcitx5-chinese-addons.git
[submodule "plugin/anthy/src/main/cpp/anthy-cmake"]
path = plugin/anthy/src/main/cpp/anthy-cmake
- url = git@github.com:fcitx5-android/anthy-cmake.git
+ url = https://github.com/fcitx5-android/anthy-cmake.git
[submodule "plugin/anthy/src/main/cpp/fcitx5-anthy"]
path = plugin/anthy/src/main/cpp/fcitx5-anthy
- url = git@github.com:fcitx/fcitx5-anthy.git
+ url = https://github.com/fcitx/fcitx5-anthy.git
[submodule "plugin/unikey/src/main/cpp/fcitx5-unikey"]
path = plugin/unikey/src/main/cpp/fcitx5-unikey
- url = git@github.com:fcitx/fcitx5-unikey.git
+ url = https://github.com/fcitx/fcitx5-unikey.git
[submodule "plugin/rime/src/main/cpp/fcitx5-rime"]
path = plugin/rime/src/main/cpp/fcitx5-rime
- url = git@github.com:fcitx/fcitx5-rime.git
+ url = https://github.com/fcitx/fcitx5-rime.git
[submodule "plugin/rime/src/main/cpp/rime-prelude"]
path = plugin/rime/src/main/cpp/rime-prelude
- url = git@github.com:rime/rime-prelude.git
+ url = https://github.com/rime/rime-prelude.git
[submodule "plugin/rime/src/main/cpp/rime-essay"]
path = plugin/rime/src/main/cpp/rime-essay
- url = git@github.com:rime/rime-essay.git
+ url = https://github.com/rime/rime-essay.git
[submodule "plugin/rime/src/main/cpp/rime-luna-pinyin"]
path = plugin/rime/src/main/cpp/rime-luna-pinyin
- url = git@github.com:rime/rime-luna-pinyin.git
+ url = https://github.com/rime/rime-luna-pinyin.git
[submodule "plugin/rime/src/main/cpp/rime-stroke"]
path = plugin/rime/src/main/cpp/rime-stroke
- url = git@github.com:rime/rime-stroke.git
+ url = https://github.com/rime/rime-stroke.git
[submodule "plugin/hangul/src/main/cpp/fcitx5-hangul"]
path = plugin/hangul/src/main/cpp/fcitx5-hangul
- url = git@github.com:fcitx/fcitx5-hangul.git
+ url = https://github.com/fcitx/fcitx5-hangul.git
[submodule "plugin/chewing/src/main/cpp/fcitx5-chewing"]
path = plugin/chewing/src/main/cpp/fcitx5-chewing
- url = git@github.com:fcitx/fcitx5-chewing.git
+ url = https://github.com/fcitx/fcitx5-chewing.git
[submodule "plugin/sayura/src/main/cpp/fcitx5-sayura"]
path = plugin/sayura/src/main/cpp/fcitx5-sayura
- url = git@github.com:fcitx/fcitx5-sayura.git
+ url = https://github.com/fcitx/fcitx5-sayura.git
[submodule "plugin/jyutping/src/main/cpp/libime-jyutping"]
path = plugin/jyutping/src/main/cpp/libime-jyutping
- url = git@github.com:fcitx/libime-jyutping.git
+ url = https://github.com/fcitx/libime-jyutping.git
[submodule "plugin/clipboard-filter/ClearURLsRules"]
path = plugin/clipboard-filter/ClearURLsRules
- url = git@github.com:ClearURLs/Rules.git
+ url = https://github.com/ClearURLs/Rules.git
+[submodule "plugin/thai/src/main/cpp/fcitx5-libthai"]
+ path = plugin/thai/src/main/cpp/fcitx5-libthai
+ url = https://github.com/fcitx/fcitx5-libthai
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index b38416157..cf0517db7 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,5 +1,40 @@
+
+
+
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 5c40d32cd..fa009839e 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -13,6 +13,7 @@
fcitx
fmtlib
icuuid
+ inputmethodservice
iostreams
iter
jbytes
@@ -21,6 +22,7 @@
jstring
jyutping
kawaii
+ keypress
lgpl
libevent
libime
@@ -37,11 +39,13 @@
shijienihao
shuangpin
sinhala
+ snackbar
spdx
stringutils
unfocus
unikey
zhuyin
+ zlib
\ No newline at end of file
diff --git a/README.md b/README.md
index a628669ef..85f9bba0f 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ GitHub: [)
- Korean (via [Hangul Plugin](./plugin/hangul))
- Sinhala (via [Sayura Plugin](./plugin/sayura))
+- Thai (via [Thai Plugin](./plugin/thai))
- Generic (via [RIME Plugin](./plugin/rime), supports importing custom schemas)
### Implemented Features
@@ -35,7 +36,7 @@ GitHub: [
- Expandable candidate view
- Clipboard management (plain text only)
-- Theming (custom color scheme and background image)
+- Theming (custom color scheme, background image and dynamic color aka monet color after Android 12)
- Popup preview on key press
- Long press popup keyboard for convenient symbol input
- Symbol and Emoji picker
@@ -68,7 +69,7 @@ Discuss on Telegram: [@fcitx5_android_group](https://t.me/fcitx5_android_group)
### Dependencies
-- Android SDK Platform & Build-Tools 34.
+- Android SDK Platform & Build-Tools 35.
- Android NDK (Side by side) 25 & CMake 3.22.1, they can be installed using SDK Manager in Android Studio or `sdkmanager` command line.
- [KDE/extra-cmake-modules](https://github.com/KDE/extra-cmake-modules)
- GNU Gettext >= 0.20 (for `msgfmt` binary; or install `appstream` if you really have to use gettext <= 0.19.)
@@ -95,29 +96,6 @@ git clone git@github.com:fcitx5-android/fcitx5-android.git
git submodule update --init --recursive
```
-
-On Windows, you may need to regenerate symlinks to submodules.
-
-Run in PowerShell:
-
-```powershell
-Remove-Item -Recurse app/src/main/assets/usr/share, plugin/hangul/src/main/assets/usr/share/libhangul, plugin/chewing/src/main/assets/usr/share/libchewing, plugin/jyutping/src/main/assets/usr/share/libime
-```
-
-Or Command Prompt:
-
-```bat
-RD /S /Q app\src\main\assets\usr\share plugin\hangul\src\main\assets\usr\share\libhangul plugin\chewing\src\main\assets\usr\share\libchewing plugin\jyutping\src\main\assets\usr\share\libime
-```
-
-Then let `git` regenerate symlinks:
-
-```shell
-git checkout -- .
-```
-
-
-
Install `extra-cmake-modules` and `gettext` with your system package manager:
```shell
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e54b4a83f..1f435a29b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -67,17 +67,18 @@ kotlin {
}
}
-aboutLibraries {
- configPath = "app/licenses"
-}
-
fcitxComponent {
- installLibraries = listOf(
+ includeLibs = listOf(
"fcitx5",
"fcitx5-lua",
"libime",
"fcitx5-chinese-addons"
)
+ // exclude (delete immediately after install) tables that nobody would use
+ excludeFiles = listOf("cangjie", "erbi", "qxm", "wanfeng").map {
+ "usr/share/fcitx5/inputmethod/$it.conf"
+ }
+ installPrebuiltAssets = true
}
ksp {
@@ -116,7 +117,8 @@ dependencies {
implementation(libs.androidx.startup)
implementation(libs.androidx.viewpager2)
implementation(libs.material)
- implementation(libs.arrow)
+ implementation(libs.arrow.core)
+ implementation(libs.arrow.functions)
implementation(libs.imagecropper)
implementation(libs.flexbox)
implementation(libs.dependency)
@@ -138,7 +140,6 @@ dependencies {
androidTestImplementation(libs.junit)
}
-@Suppress("UnstableApiUsage")
configurations {
all {
// remove Baseline Profile Installer or whatever it is...
diff --git a/app/licenses/libraries/boost.json b/app/licenses/libraries/boost.json
index 7a2285fb6..cebc09b62 100644
--- a/app/licenses/libraries/boost.json
+++ b/app/licenses/libraries/boost.json
@@ -1,6 +1,6 @@
{
"uniqueId": "boostorg/boost",
- "artifactVersion": "1.83.0",
+ "artifactVersion": "1.86.0",
"description": "Free peer-reviewed portable C++ source libraries",
"name": "boostorg/boost",
"website": "https://www.boost.org/",
diff --git a/app/licenses/libraries/fcitx5-chinese-addons.json b/app/licenses/libraries/fcitx5-chinese-addons.json
index aef4c5662..8200bcbed 100644
--- a/app/licenses/libraries/fcitx5-chinese-addons.json
+++ b/app/licenses/libraries/fcitx5-chinese-addons.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-chinese-addons",
- "artifactVersion": "5.1.5",
+ "artifactVersion": "5.1.7",
"description": "Chinese related addon for fcitx5",
"name": "fcitx/fcitx5-chinese-addons",
"website": "https://github.com/fcitx/fcitx5-chinese-addons",
diff --git a/app/licenses/libraries/fcitx5-lua.json b/app/licenses/libraries/fcitx5-lua.json
index 69033b46c..e3ff2ca38 100644
--- a/app/licenses/libraries/fcitx5-lua.json
+++ b/app/licenses/libraries/fcitx5-lua.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-lua",
- "artifactVersion": "5.0.13",
+ "artifactVersion": "5.0.14",
"description": "Lua support for fcitx5",
"name": "fcitx/fcitx5-lua",
"website": "https://github.com/fcitx/fcitx5-lua",
diff --git a/app/licenses/libraries/fcitx5.json b/app/licenses/libraries/fcitx5.json
index 6545963ba..f20f85043 100644
--- a/app/licenses/libraries/fcitx5.json
+++ b/app/licenses/libraries/fcitx5.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5",
- "artifactVersion": "5.1.9",
+ "artifactVersion": "5.1.12",
"description": "Next generation of fcitx",
"name": "fcitx/fcitx5",
"website": "https://github.com/fcitx/fcitx5",
diff --git a/app/licenses/libraries/fmt.json b/app/licenses/libraries/fmt.json
index b69becddf..07f2dbdc1 100644
--- a/app/licenses/libraries/fmt.json
+++ b/app/licenses/libraries/fmt.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fmtlib/fmt",
- "artifactVersion": "9.1.0",
+ "artifactVersion": "11.0.2",
"description": "Open-source formatting library for C++",
"name": "fmtlib/fmt",
"website": "https://fmt.dev",
diff --git a/app/licenses/libraries/libime.json b/app/licenses/libraries/libime.json
index 1522073e9..18507f632 100644
--- a/app/licenses/libraries/libime.json
+++ b/app/licenses/libraries/libime.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/libime",
- "artifactVersion": "1.1.7",
+ "artifactVersion": "1.1.10",
"description": "library to support generic input method implementation",
"name": "fcitx/libime",
"website": "https://github.com/fcitx/libime",
diff --git a/app/licenses/libraries/libintl-lite.json b/app/licenses/libraries/libintl-lite.json
index 86daf4cce..e08b58baf 100644
--- a/app/licenses/libraries/libintl-lite.json
+++ b/app/licenses/libraries/libintl-lite.json
@@ -1,6 +1,6 @@
{
"uniqueId": "j-jorge/libintl-lite",
- "artifactVersion": "5750d92",
+ "artifactVersion": "ba15146",
"description": "simple (but less powerful) GNU gettext libintl replacement",
"name": "j-jorge/libintl-lite",
"website": "https://github.com/j-jorge/libintl-lite",
diff --git a/app/licenses/libraries/libuv.json b/app/licenses/libraries/libuv.json
index af34bce99..65d992b93 100644
--- a/app/licenses/libraries/libuv.json
+++ b/app/licenses/libraries/libuv.json
@@ -1,6 +1,6 @@
{
"uniqueId": "libuv/libuv",
- "artifactVersion": "1.47.0",
+ "artifactVersion": "1.49.2",
"description": "Cross-platform asynchronous I/O",
"name": "libuv/libuv",
"website": "https://libuv.org/",
diff --git a/app/licenses/libraries/lua.json b/app/licenses/libraries/lua.json
index 386077cda..11c9ef4c7 100644
--- a/app/licenses/libraries/lua.json
+++ b/app/licenses/libraries/lua.json
@@ -1,6 +1,6 @@
{
"uniqueId": "lua/lua",
- "artifactVersion": "5.4.6",
+ "artifactVersion": "5.4.7",
"description": "Powerful lightweight programming language designed for extending applications",
"name": "lua/lua",
"website": "https://www.lua.org/",
diff --git a/app/licenses/libraries/opencc.json b/app/licenses/libraries/opencc.json
index 5aa3ea77b..ed4109ee0 100644
--- a/app/licenses/libraries/opencc.json
+++ b/app/licenses/libraries/opencc.json
@@ -1,6 +1,6 @@
{
"uniqueId": "BYVoid/OpenCC",
- "artifactVersion": "1.1.7",
+ "artifactVersion": "1.1.9",
"description": "opensource project for conversions between Traditional Chinese, Simplified Chinese and Japanese Kanji (Shinjitai).",
"name": "BYVoid/OpenCC",
"website": "https://opencc.byvoid.com/",
diff --git a/app/org.fcitx.fcitx5.android.yml b/app/org.fcitx.fcitx5.android.yml
index c99e68d07..7c2ffc2eb 100644
--- a/app/org.fcitx.fcitx5.android.yml
+++ b/app/org.fcitx.fcitx5.android.yml
@@ -21,10 +21,9 @@ Builds:
sudo:
- apt-get update
- apt-get install -y g++ libtool make automake gettext bzip2 xz-utils zstd pkg-config
- cmake extra-cmake-modules ninja-build libfmt-dev libboost-all-dev libfcitx5utils-dev opencc openjdk-17-jdk-headless
- ghc cabal-install libghc-shake-dev libghc-aeson-pretty-dev libghc-js-flot-data haskell-js-dgtable-utils
- - update-java-alternatives -a
- - apt-get install -y -t bullseye-backports fcitx5-modules
+ cmake extra-cmake-modules ninja-build libfmt-dev libsystemd-dev libboost-all-dev
+ ghc cabal-install libghc-shake-dev libghc-aeson-pretty-dev libghc-js-flot-data haskell-js-dgtable-utils
+ python-is-python3 opencc
gradle:
- yes
binary: https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/lastSuccessfulBuild/artifact/out/org.fcitx.fcitx5.android-%v-%abi-release.apk
@@ -33,7 +32,7 @@ Builds:
rm:
- lib/fcitx5/src/main/cpp/prebuilt
prebuild:
- - sdkmanager 'cmake;3.22.1'
+ - sdkmanager 'cmake;3.31.6'
- sed -i -e '/ImportQualifiedPost/d' $$fcitx5-android-prebuilder$$/src/Main.hs
- sed -i -e 's/import \(.*\) qualified as/import qualified \1 as/g' $$fcitx5-android-prebuilder$$/src/*.hs
- sed -i -e 's|https://maven.pkg.github.com|https://jitpack.io|g' ../build-logic/convention/build.gradle.kts
@@ -44,12 +43,11 @@ Builds:
- build-logic/convention/build
build:
- pushd $$fcitx5-android-prebuilder$$
- - ABI=%abi ANDROID_NDK_ROOT=$$NDK$$ CMAKE_VERSION=3.22.1 ANDROID_PLATFORM=23
- COMP_SPELL_DICT=/usr/lib/x86_64-linux-gnu/fcitx5/libexec/comp-spell-dict
- ./build-cabal -j spell-dict fmt libuv libintl-lite boost marisa opencc libime lua chinese-addons-data zstd
+ - ABI=%abi ANDROID_NDK_ROOT=$$NDK$$ CMAKE_VERSION=3.31.6 ANDROID_PLATFORM=23
+ ./build-cabal -j app
- popd
- mv $$fcitx5-android-prebuilder$$/build ../lib/fcitx5/src/main/cpp/prebuilt
- ndk: 25.2.9519653
+ ndk: 28.0.13004108
gradleprops:
- buildABI=%abi
- buildTimestamp=%ts
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7a5aecc0d..fcc089b60 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -62,12 +62,9 @@
android:configChanges="orientation|screenSize"
android:exported="false"
android:label="@string/edit_theme" />
-
+ android:name=".ui.main.CropImageActivity"
+ android:exported="false" />
+
@@ -102,6 +103,10 @@
+
+
+
+
@@ -152,12 +157,22 @@
+
+
+
+
+
+
${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- # fcitx5-lua
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- # fcitx5-chinese-addons
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
COMMENT "Copying fcitx5 module libraries to :app"
)
+
+# install prebuilt assets
+install(FILES "${PREBUILT_DIR}/spell-dict/en_dict.fscd" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/spell" COMPONENT prebuilt-assets)
+install(FILES "${PREBUILT_DIR}/chinese-addons-data/pinyin/chaizi.dict" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/pinyin" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/chinese-addons-data/pinyinhelper" DESTINATION "${FCITX_INSTALL_PKGDATADIR}" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/libime/table" DESTINATION "${FCITX_INSTALL_PKGDATADIR}" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/libime/data/" DESTINATION "${FCITX_INSTALL_DATADIR}/libime" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/opencc/data/" DESTINATION "${FCITX_INSTALL_DATADIR}/opencc" COMPONENT prebuilt-assets)
diff --git a/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp b/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp
deleted file mode 100644
index 26ae10387..000000000
--- a/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer
- * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
- * SPDX-FileComment: Modified from https://github.com/fcitx/fcitx5/blob/5.1.1/src/lib/fcitx/addonloader.cpp
- */
-#include "androidaddonloader.h"
-
-#define FCITX_LIBRARY_SUFFIX ".so"
-
-namespace fcitx {
-
-AndroidSharedLibraryLoader::AndroidSharedLibraryLoader(AndroidLibraryDependency dependency)
- : dependency_(std::move(dependency)) {}
-
-AddonInstance *AndroidSharedLibraryLoader::load(const AddonInfo &info,
- AddonManager *manager) {
- auto iter = registry_.find(info.uniqueName());
- if (iter == registry_.end()) {
- std::string libname = info.library();
- Flags flag = LibraryLoadHint::DefaultHint;
- if (stringutils::startsWith(libname, "export:")) {
- libname = libname.substr(7);
- flag |= LibraryLoadHint::ExportExternalSymbolsHint;
- }
- auto file = libname + FCITX_LIBRARY_SUFFIX;
- auto libs = standardPath_.locateAll(StandardPath::Type::Addon, file);
- if (libs.empty()) {
- FCITX_ERROR() << "Could not locate library " << file
- << " for addon " << info.uniqueName() << ".";
- }
- // ========== Android specific start ========== //
- auto deps = dependency_.find(libname);
- if (deps != dependency_.end()) {
- for (const auto &dep: deps->second) {
- auto depFile = dep + FCITX_LIBRARY_SUFFIX;
- auto depPaths = standardPath_.locateAll(StandardPath::Type::Addon, depFile);
- if (depPaths.empty()) {
- FCITX_ERROR() << "Could not locate dependency " << depFile
- << " for library " << file << ".";
- } else {
- for (const auto &depPath: depPaths) {
- Library depLib(depPath);
- if (!depLib.load()) {
- FCITX_ERROR() << "Failed to load dependency " << depPath
- << " for library " << file << ".";
- } else {
- FCITX_INFO() << "Loaded dependency " << depFile
- << " for library " << file << ".";
- break;
- }
- }
- }
- }
- }
- // ========== Android specific end ========== //
- for (const auto &libraryPath: libs) {
- Library lib(libraryPath);
- if (!lib.load(flag)) {
- FCITX_ERROR()
- << "Failed to load library for addon " << info.uniqueName()
- << " on " << libraryPath << ". Error: " << lib.error();
- continue;
- }
- try {
- registry_.emplace(
- info.uniqueName(),
- std::make_unique(std::move(lib)));
- } catch (const std::exception &e) {
- }
- break;
- }
- iter = registry_.find(info.uniqueName());
- }
-
- if (iter == registry_.end()) {
- return nullptr;
- }
-
- try {
- return iter->second->factory()->create(manager);
- } catch (const std::exception &e) {
- FCITX_ERROR() << "Failed to create addon: " << info.uniqueName() << " "
- << e.what();
- } catch (...) {
- FCITX_ERROR() << "Failed to create addon: " << info.uniqueName();
- }
- return nullptr;
-}
-
-}
diff --git a/app/src/main/cpp/androidaddonloader/androidaddonloader.h b/app/src/main/cpp/androidaddonloader/androidaddonloader.h
deleted file mode 100644
index ff3a444b9..000000000
--- a/app/src/main/cpp/androidaddonloader/androidaddonloader.h
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer
- * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
- * SPDX-FileComment: Modified from https://github.com/fcitx/fcitx5/blob/5.1.1/src/lib/fcitx/addonloader_p.h
- */
-#ifndef FCITX5_ANDROID_ANDROIDADDONLOADER_H
-#define FCITX5_ANDROID_ANDROIDADDONLOADER_H
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace fcitx {
-
-class AndroidSharedLibraryFactory {
-public:
- AndroidSharedLibraryFactory(Library lib) : library_(std::move(lib)) {
- auto *funcPtr = library_.resolve("fcitx_addon_factory_instance");
- if (!funcPtr) {
- throw std::runtime_error(library_.error());
- }
- auto func = Library::toFunction(funcPtr);
- factory_ = func();
- if (!factory_) {
- throw std::runtime_error("Failed to get a factory");
- }
- }
-
- AddonFactory *factory() { return factory_; }
-
-private:
- Library library_;
- AddonFactory *factory_;
-};
-
-typedef std::unordered_map> AndroidLibraryDependency;
-
-class AndroidSharedLibraryLoader : public AddonLoader {
-public:
- AndroidSharedLibraryLoader(AndroidLibraryDependency dependency);
- ~AndroidSharedLibraryLoader() = default;
- std::string type() const override { return "SharedLibrary"; }
-
- AddonInstance *load(const AddonInfo &info, AddonManager *manager) override;
-
-private:
- StandardPath standardPath_;
- std::unordered_map> registry_;
- AndroidLibraryDependency dependency_;
-};
-
-}
-
-#endif //FCITX5_ANDROID_ANDROIDADDONLOADER_H
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.cpp b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
index 37ceff737..620452604 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.cpp
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
@@ -4,6 +4,7 @@
*/
#include
#include
+#include
#include
#include
#include
@@ -49,7 +50,8 @@ class AndroidInputContext : public InputContextV2 {
const int before = -offset;
const int after = offset + static_cast(size);
if (before < 0 || after < 0) {
- FCITX_WARN() << "Invalid deleteSurrounding request: offset=" << offset << ", size=" << size;
+ FCITX_WARN() << "Invalid deleteSurrounding request: offset=" << offset << ", size="
+ << size;
return;
}
frontend_->deleteSurrounding(before, after);
@@ -66,9 +68,12 @@ class AndroidInputContext : public InputContextV2 {
filterText(ip.auxUp()),
filterText(ip.auxDown())
);
+ }
+
+ void updateCandidatesBulk() {
std::vector candidates;
int size = 0;
- const auto &list = ip.candidateList();
+ const auto &list = inputPanel().candidateList();
if (list) {
const auto &bulk = list->toBulk();
if (bulk) {
@@ -96,7 +101,37 @@ class AndroidInputContext : public InputContextV2 {
frontend_->updateCandidateList(candidates, size);
}
- bool selectCandidate(int idx) {
+ void updateCandidatesPaged() {
+ const auto &list = inputPanel().candidateList();
+ if (!list) {
+ frontend_->updatePagedCandidate(PagedCandidateEntity::Empty);
+ return;
+ }
+ int cursorIndex = list->cursorIndex();
+ CandidateLayoutHint layoutHint = list->layoutHint();
+ bool hasPrev = false;
+ bool hasNext = false;
+ const auto &pageable = list->toPageable();
+ if (pageable) {
+ hasPrev = pageable->hasPrev();
+ hasNext = pageable->hasNext();
+ }
+ int size = list->size();
+ std::vector candidates;
+ candidates.reserve(size);
+ for (int i = 0; i < size; i++) {
+ const auto &c = list->candidate(i);
+ candidates.emplace_back(
+ filterString(list->label(i)),
+ filterString(c.text()),
+ filterString(c.comment())
+ );
+ }
+ PagedCandidateEntity paged(candidates, cursorIndex, layoutHint, hasPrev, hasNext);
+ frontend_->updatePagedCandidate(paged);
+ }
+
+ bool selectCandidateBulk(int idx) {
const auto &list = inputPanel().candidateList();
if (!list) {
return false;
@@ -115,6 +150,20 @@ class AndroidInputContext : public InputContextV2 {
return true;
}
+ bool selectCandidatePaged(int idx) {
+ const auto &list = inputPanel().candidateList();
+ if (!list) {
+ return false;
+ }
+ try {
+ list->candidate(idx).select(this);
+ } catch (const std::invalid_argument &e) {
+ FCITX_WARN() << "selectCandidate index out of range";
+ return false;
+ }
+ return true;
+ }
+
std::vector getCandidates(const int offset, const int limit) {
std::vector candidates;
const auto &list = inputPanel().candidateList();
@@ -142,6 +191,77 @@ class AndroidInputContext : public InputContextV2 {
return candidates;
}
+ std::vector getCandidateAction(const int idx) {
+ std::vector actions;
+ const auto &list = inputPanel().candidateList();
+ if (list) {
+ const auto &actionable = list->toActionable();
+ if (actionable) {
+ if (idx >= list->size()) {
+ const auto &bulk = list->toBulk();
+ if (bulk) {
+ try {
+ const auto &c = bulk->candidateFromAll(idx);
+ for (const auto &a: actionable->candidateActions(c)) {
+ actions.emplace_back(a);
+ }
+ } catch (const std::exception &e) {
+ FCITX_WARN() << "getCandidateAction(" << idx << ") failed:" << e.what();
+ }
+ }
+ } else {
+ const auto &c = list->candidate(idx);
+ for (const auto &a: actionable->candidateActions(c)) {
+ actions.emplace_back(a);
+ }
+ }
+ }
+ }
+ return actions;
+ }
+
+ void triggerCandidateAction(const int idx, const int actionIdx) {
+ const auto &list = inputPanel().candidateList();
+ if (!list) return;
+ const auto &actionable = list->toActionable();
+ if (!actionable) return;
+ if (idx >= list->size()) {
+ const auto &bulk = list->toBulk();
+ if (bulk) {
+ try {
+ const auto &c = bulk->candidateFromAll(idx);
+ actionable->triggerAction(c, actionIdx);
+ } catch (const std::exception &e) {
+ FCITX_WARN() << "triggerCandidateAction(" << idx << ") failed:" << e.what();
+ }
+ }
+ } else {
+ const auto &c = list->candidate(idx);
+ actionable->triggerAction(c, actionIdx);
+ }
+ }
+
+ void offsetCandidatePage(int delta) {
+ if (delta == 0) {
+ return;
+ }
+ const auto &list = inputPanel().candidateList();
+ if (!list) {
+ return;
+ }
+ const auto &pageable = list->toPageable();
+ if (!pageable) {
+ return;
+ }
+ if (delta > 0 && pageable->hasNext()) {
+ pageable->next();
+ updateUserInterface(UserInterfaceComponent::InputPanel);
+ } else if (delta < 0 && pageable->hasPrev()) {
+ pageable->prev();
+ updateUserInterface(UserInterfaceComponent::InputPanel);
+ }
+ }
+
private:
AndroidFrontend *frontend_;
int uid_;
@@ -160,7 +280,8 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
focusGroup_("android", instance->inputContextManager()),
activeIC_(nullptr),
icCache_(),
- eventHandlers_() {
+ eventHandlers_(),
+ pagingMode_(0) {
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextInputMethodActivated,
EventWatcherPhase::Default,
@@ -176,7 +297,14 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
auto &e = static_cast(event);
switch (e.component()) {
case UserInterfaceComponent::InputPanel: {
- if (activeIC_) activeIC_->updateInputPanel();
+ if (activeIC_) {
+ activeIC_->updateInputPanel();
+ if (pagingMode_ == 0) {
+ activeIC_->updateCandidatesBulk();
+ } else {
+ activeIC_->updateCandidatesPaged();
+ }
+ }
break;
}
case UserInterfaceComponent::StatusArea: {
@@ -225,29 +353,21 @@ void AndroidFrontend::releaseInputContext(const int uid) {
bool AndroidFrontend::selectCandidate(int idx) {
if (!activeIC_) return false;
- return activeIC_->selectCandidate(idx);
-}
-
-bool AndroidFrontend::forgetCandidate(int idx) {
- if (!activeIC_) return false;
- // check current engine, only pinyin and table engine support deleting words
- auto *entry = instance_->inputMethodEntry(activeIC_);
- if (entry->addon() != "pinyin" && entry->addon() != "table") return false;
- // do we have candidate list?
- auto list = activeIC_->inputPanel().candidateList();
- if (!list) return false;
- // Ctrl+7 to activate forget candidate mode
- Key key(FcitxKey_7, Flags(KeyState::Ctrl));
- KeyEvent pressEvent(activeIC_, key, false);
- auto handled = activeIC_->keyEvent(pressEvent);
- if (handled) {
- KeyEvent releaseEvent(activeIC_, key, true);
- activeIC_->keyEvent(releaseEvent);
+ if (pagingMode_) {
+ return activeIC_->selectCandidatePaged(idx);
} else {
- // something went wrong
- return false;
+ return activeIC_->selectCandidateBulk(idx);
}
- return activeIC_->selectCandidate(idx);
+}
+
+std::vector AndroidFrontend::getCandidateActions(const int idx) {
+ if (!activeIC_) return {};
+ return activeIC_->getCandidateAction(idx);
+}
+
+void AndroidFrontend::triggerCandidateAction(const int idx, const int actionIdx) {
+ if (!activeIC_) return;
+ activeIC_->triggerCandidateAction(idx, actionIdx);
}
bool AndroidFrontend::isInputPanelEmpty() {
@@ -320,6 +440,19 @@ void AndroidFrontend::showToast(const std::string &s) {
toastCallback(s);
}
+void AndroidFrontend::setCandidatePagingMode(const int mode) {
+ pagingMode_ = mode;
+}
+
+void AndroidFrontend::updatePagedCandidate(const PagedCandidateEntity &paged) {
+ pagedCandidateCallback(paged);
+}
+
+void AndroidFrontend::offsetCandidatePage(int delta) {
+ if (!activeIC_) return;
+ activeIC_->offsetCandidatePage(delta);
+}
+
void AndroidFrontend::setCommitStringCallback(const CommitStringCallback &callback) {
commitStringCallback = callback;
}
@@ -352,6 +485,10 @@ void AndroidFrontend::setToastCallback(const ToastCallback &callback) {
toastCallback = callback;
}
+void AndroidFrontend::setPagedCandidateCallback(const PagedCandidateCallback &callback) {
+ pagedCandidateCallback = callback;
+}
+
class AndroidFrontendFactory : public AddonFactory {
public:
AddonInstance *create(AddonManager *manager) override {
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.h b/app/src/main/cpp/androidfrontend/androidfrontend.h
index 796973d7c..9b9db800b 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.h
@@ -2,8 +2,9 @@
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
*/
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_H_
+
+#ifndef FCITX5_ANDROID_ANDROIDFRONTEND_H
+#define FCITX5_ANDROID_ANDROIDFRONTEND_H
#include
#include
@@ -18,7 +19,7 @@ class AndroidInputContext;
class AndroidFrontend : public AddonInstance {
public:
- AndroidFrontend(Instance *instance);
+ explicit AndroidFrontend(Instance *instance);
Instance *instance() { return instance_; }
@@ -27,6 +28,7 @@ class AndroidFrontend : public AddonInstance {
void updateClientPreedit(const Text &clientPreedit);
void updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown);
void releaseInputContext(const int uid);
+ void updatePagedCandidate(const PagedCandidateEntity &paged);
void keyEvent(const Key &key, bool isRelease, const int timestamp);
void forwardKey(const Key &key, bool isRelease);
@@ -37,11 +39,15 @@ class AndroidFrontend : public AddonInstance {
void focusInputContext(bool focus);
void activateInputContext(const int uid, const std::string &pkgName);
void deactivateInputContext(const int uid);
- InputContext *activeInputContext() const;
+ [[nodiscard]] InputContext *activeInputContext() const;
void setCapabilityFlags(uint64_t flag);
std::vector getCandidates(const int offset, const int limit);
+ std::vector getCandidateActions(const int idx);
+ void triggerCandidateAction(const int idx, const int actionIdx);
void deleteSurrounding(const int before, const int after);
void showToast(const std::string &s);
+ void setCandidatePagingMode(const int mode);
+ void offsetCandidatePage(int delta);
void setCandidateListCallback(const CandidateListCallback &callback);
void setCommitStringCallback(const CommitStringCallback &callback);
void setPreeditCallback(const ClientPreeditCallback &callback);
@@ -51,7 +57,7 @@ class AndroidFrontend : public AddonInstance {
void setStatusAreaUpdateCallback(const StatusAreaUpdateCallback &callback);
void setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback);
void setToastCallback(const ToastCallback &callback);
- bool forgetCandidate(int idx);
+ void setPagedCandidateCallback(const PagedCandidateCallback &callback);
private:
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, keyEvent);
@@ -65,7 +71,11 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, deactivateInputContext);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCapabilityFlags);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, getCandidates);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, getCandidateActions);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, triggerCandidateAction);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, showToast);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCandidatePagingMode);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, offsetCandidatePage);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCandidateListCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCommitStringCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPreeditCallback);
@@ -75,13 +85,14 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setStatusAreaUpdateCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setToastCallback);
- FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, forgetCandidate);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPagedCandidateCallback);
Instance *instance_;
FocusGroup focusGroup_;
AndroidInputContext *activeIC_;
InputContextCache icCache_;
std::vector>> eventHandlers_;
+ int pagingMode_;
CandidateListCallback candidateListCallback = [](const std::vector &, const int) {};
CommitStringCallback commitStringCallback = [](const std::string &, const int) {};
@@ -92,7 +103,8 @@ class AndroidFrontend : public AddonInstance {
StatusAreaUpdateCallback statusAreaUpdateCallback = [] {};
DeleteSurroundingCallback deleteSurroundingCallback = [](const int, const int) {};
ToastCallback toastCallback = [](const std::string &) {};
+ PagedCandidateCallback pagedCandidateCallback = [](const PagedCandidateEntity &) {};
};
} // namespace fcitx
-#endif //_FCITX5_ANDROID_ANDROIDFRONTEND_H_
+#endif //FCITX5_ANDROID_ANDROIDFRONTEND_H
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend_public.h b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
index 76ded712b..69bcbbdd5 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend_public.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
@@ -2,13 +2,16 @@
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
*/
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+#ifndef FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
+#define FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
#include
#include
+#include
#include
+#include "../helper-types.h"
+
typedef std::function &, const int)> CandidateListCallback;
typedef std::function CommitStringCallback;
typedef std::function ClientPreeditCallback;
@@ -18,6 +21,7 @@ typedef std::function InputMethodChangeCallback;
typedef std::function StatusAreaUpdateCallback;
typedef std::function DeleteSurroundingCallback;
typedef std::function ToastCallback;
+typedef std::function PagedCandidateCallback;
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, keyEvent,
void(const fcitx::Key &, bool isRelease, const int timestamp))
@@ -52,9 +56,21 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCapabilityFlags,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, getCandidates,
std::vector(const int, const int))
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, getCandidateActions,
+ std::vector(const int))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, triggerCandidateAction,
+ void(const int, const int))
+
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, showToast,
void(const std::string &))
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCandidatePagingMode,
+ void(const int))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, offsetCandidatePage,
+ void(int))
+
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCandidateListCallback,
void(const CandidateListCallback &))
@@ -82,7 +98,7 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setToastCallback,
void(const ToastCallback &))
-FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, forgetCandidate,
- bool(int idx))
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setPagedCandidateCallback,
+ void(const PagedCandidateCallback &))
-#endif // _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+#endif // FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
index 153108750..a84d55ab7 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
@@ -108,8 +108,7 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
}
if (key.isLAZ() || key.isUAZ() || validSym ||
(!buffer.empty() && key.checkKeyList(FCITX_HYPHEN_APOS))) {
- auto text = Key::keySymToUTF8(key.sym());
- if (updateBuffer(inputContext, text)) {
+ if (updateBuffer(inputContext, event)) {
return event.filterAndAccept();
}
}
@@ -142,16 +141,16 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
auto cursor = buffer.cursor();
if (cursor > 0) {
buffer.setCursor(cursor - 1);
+ event.filterAndAccept();
+ return updateCandidate(entry, inputContext);
}
- event.filterAndAccept();
- return updateCandidate(entry, inputContext);
} else if (key.check(FcitxKey_Right) || key.check(FcitxKey_KP_Right)) {
auto cursor = buffer.cursor();
if (cursor < buffer.size()) {
buffer.setCursor(buffer.cursor() + 1);
+ event.filterAndAccept();
+ return updateCandidate(entry, inputContext);
}
- event.filterAndAccept();
- return updateCandidate(entry, inputContext);
}
}
@@ -249,14 +248,22 @@ void AndroidKeyboardEngine::resetState(InputContext *inputContext, bool fromCand
void AndroidKeyboardEngine::updateCandidate(const InputMethodEntry &entry, InputContext *inputContext) {
inputContext->inputPanel().reset();
auto *state = inputContext->propertyFor(&factory_);
+ const auto userInput = state->buffer_.userInput();
std::vector> results;
if (spell()) {
results = spell()->call(entry.languageCode(),
SpellProvider::Default,
- state->buffer_.userInput(),
+ userInput,
SpellCandidateSize);
}
auto candidateList = std::make_unique();
+ if (results.empty() || results.front().second != userInput) {
+ // TODO: comply with fcitx5 spell module's delim " _-,./?!%"
+ // it's fine in androidkeyboard because only "-" won't commit buffer
+ const auto segments = stringutils::split(userInput, "-");
+ const auto label = segments.size() > 1 ? segments.back() : userInput;
+ candidateList->append(this, Text(label), userInput);
+ }
for (const auto &result: results) {
candidateList->append(this, Text(result.first), result.second);
}
@@ -283,7 +290,7 @@ void AndroidKeyboardEngine::updateUI(InputContext *inputContext) {
inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}
-bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::string &chr) {
+bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const KeyEvent& event) {
auto *entry = instance_->inputMethodEntry(inputContext);
if (!entry) {
return false;
@@ -292,6 +299,7 @@ bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::
auto *state = inputContext->propertyFor(&factory_);
// word hint is disabled, input is password, or language not supported
if (!*config_.enableWordHint ||
+ (!*config_.hintOnPhysicalKeyboard && !event.isVirtual()) ||
(*config_.editorControlledWordHint && inputContext->capabilityFlags().test(CapabilityFlag::NoSpellCheck)) ||
inputContext->capabilityFlags().test(CapabilityFlag::Password) ||
!supportHint(entry->languageCode())) {
@@ -305,7 +313,7 @@ bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::
buffer.type(preedit);
}
- buffer.type(chr);
+ buffer.type(Key::keySymToUTF8(event.key().sym()));
if (buffer.size() >= MaxBufferSize) {
commitBuffer(inputContext);
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.h b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
index 21fd52da7..b365ac527 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.h
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
@@ -2,8 +2,8 @@
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
*/
-#ifndef _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
-#define _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
+#ifndef FCITX5_ANDROID_ANDROIDKEYBOARD_H
+#define FCITX5_ANDROID_ANDROIDKEYBOARD_H
#include
#include
@@ -30,6 +30,8 @@ FCITX_CONFIGURATION(
AndroidKeyboardEngineConfig,
Option
enableWordHint{this, "EnableWordHint", _("Enable word hint"), true};
+ Option
+ hintOnPhysicalKeyboard{this, "WordHintOnPhysicalKeyboard", _("Enable word hint when using physical keyboard"), false};
Option
editorControlledWordHint{this, "EditorControlledWordHint", _("Disable word hint based on editor attributes"), true};
Option
@@ -59,8 +61,7 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
static int constexpr MaxBufferSize = 20;
static int constexpr SpellCandidateSize = 20;
- AndroidKeyboardEngine(Instance *instance);
- ~AndroidKeyboardEngine() = default;
+ explicit AndroidKeyboardEngine(Instance *instance);
Instance *instance() { return instance_; }
@@ -96,10 +97,10 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
auto factory() { return &factory_; }
- // Return true if chr is pushed to buffer.
- // Return false if chr will be skipped by buffer, usually this means caller
- // need to call commit buffer and forward chr manually.
- bool updateBuffer(InputContext *inputContext, const std::string &chr);
+ // Return true if event is pushed to buffer.
+ // Return false if event will be skipped by buffer, usually this means caller
+ // need to call commit buffer and forward event manually.
+ bool updateBuffer(InputContext *inputContext, const KeyEvent& event);
// Commit current buffer, also reset the state.
// See also preeditString().
@@ -133,4 +134,4 @@ class AndroidKeyboardEngineFactory : public AddonFactory {
}
-#endif //_FCITX5_ANDROID_ANDROIDKEYBOARD_H_
+#endif //FCITX5_ANDROID_ANDROIDKEYBOARD_H
diff --git a/app/src/main/cpp/androidnotification/androidnotification.h b/app/src/main/cpp/androidnotification/androidnotification.h
index 4dbd35ba9..242e2857b 100644
--- a/app/src/main/cpp/androidnotification/androidnotification.h
+++ b/app/src/main/cpp/androidnotification/androidnotification.h
@@ -22,12 +22,11 @@ namespace fcitx {
FCITX_CONFIGURATION(NotificationsConfig,
fcitx::Option> hiddenNotifications{
this, "HiddenNotifications",
- _("Hidden Notifications")};);
+ _("Hidden Notifications")};)
class Notifications final : public AddonInstance {
public:
explicit Notifications(Instance *instance);
- ~Notifications() override = default;
Instance *instance() { return instance_; }
diff --git a/app/src/main/cpp/helper-types.h b/app/src/main/cpp/helper-types.h
index a0be956c9..1f13c9494 100644
--- a/app/src/main/cpp/helper-types.h
+++ b/app/src/main/cpp/helper-types.h
@@ -8,6 +8,12 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+
+#include
class InputMethodStatus {
public:
@@ -83,4 +89,63 @@ class ActionEntity {
}
};
+class CandidateActionEntity {
+public:
+ int id;
+ std::string text;
+ bool isSeparator;
+ std::string icon;
+ bool isCheckable;
+ bool isChecked;
+
+ explicit CandidateActionEntity(const fcitx::CandidateAction &act) :
+ id(act.id()),
+ text(act.text()),
+ isSeparator(act.isSeparator()),
+ icon(act.icon()),
+ isCheckable(act.isCheckable()),
+ isChecked(act.isChecked()) {}
+};
+
+class CandidateEntity {
+public:
+ std::string label;
+ std::string text;
+ std::string comment;
+
+ explicit CandidateEntity(std::string label, std::string text, std::string comment) :
+ label(std::move(label)),
+ text(std::move(text)),
+ comment(std::move(comment)) {}
+};
+
+class PagedCandidateEntity {
+public:
+ std::vector candidates;
+ int cursorIndex;
+ fcitx::CandidateLayoutHint layoutHint;
+ bool hasPrev;
+ bool hasNext;
+
+ explicit PagedCandidateEntity(std::vector candidates,
+ int cursorIndex,
+ fcitx::CandidateLayoutHint layoutHint,
+ bool hasPrev,
+ bool hasNext) :
+ candidates(std::move(candidates)),
+ cursorIndex(cursorIndex),
+ layoutHint(layoutHint),
+ hasPrev(hasPrev),
+ hasNext(hasNext) {}
+
+ static PagedCandidateEntity Empty;
+
+private:
+ PagedCandidateEntity() :
+ candidates({}), cursorIndex(-1), layoutHint(fcitx::CandidateLayoutHint::NotSet),
+ hasPrev(false), hasNext(false) {}
+};
+
+PagedCandidateEntity PagedCandidateEntity::Empty = PagedCandidateEntity();
+
#endif //FCITX5_ANDROID_HELPER_TYPES_H
diff --git a/app/src/main/cpp/jni-utils.h b/app/src/main/cpp/jni-utils.h
index 1e8bec212..6807b531d 100644
--- a/app/src/main/cpp/jni-utils.h
+++ b/app/src/main/cpp/jni-utils.h
@@ -77,10 +77,10 @@ class JString {
class JEnv {
private:
- JNIEnv *env;
+ JNIEnv *env = nullptr;
public:
- JEnv(JavaVM *jvm) {
+ explicit JEnv(JavaVM *jvm) {
if (jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
jvm->AttachCurrentThread(&env, nullptr);
}
@@ -138,7 +138,13 @@ class GlobalRefSingleton {
jfieldID PinyinCustomPhraseOrder;
jfieldID PinyinCustomPhraseValue;
- GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
+ jclass CandidateAction;
+ jmethodID CandidateActionInit;
+
+ jclass Candidate;
+ jmethodID CandidateInit;
+
+ explicit GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
JNIEnv *env;
jvm->AttachCurrentThread(&env, nullptr);
@@ -184,9 +190,15 @@ class GlobalRefSingleton {
PinyinCustomPhraseKey = env->GetFieldID(PinyinCustomPhrase, "key", "Ljava/lang/String;");
PinyinCustomPhraseOrder = env->GetFieldID(PinyinCustomPhrase, "order", "I");
PinyinCustomPhraseValue = env->GetFieldID(PinyinCustomPhrase, "value", "Ljava/lang/String;");
+
+ CandidateAction = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/CandidateAction")));
+ CandidateActionInit = env->GetMethodID(CandidateAction, "", "(ILjava/lang/String;ZLjava/lang/String;ZZ)V");
+
+ Candidate = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/FcitxEvent$Candidate")));
+ CandidateInit = env->GetMethodID(Candidate, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
}
- const JEnv AttachEnv() const { return JEnv(jvm); }
+ [[nodiscard]] JEnv AttachEnv() const { return JEnv(jvm); }
};
extern GlobalRefSingleton *GlobalRef;
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
index ca4be07ac..bdeb17191 100644
--- a/app/src/main/cpp/native-lib.cpp
+++ b/app/src/main/cpp/native-lib.cpp
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
#include
@@ -41,7 +41,6 @@
#include "customphrase.h"
#include "androidfrontend/androidfrontend_public.h"
-#include "androidaddonloader/androidaddonloader.h"
#include "jni-utils.h"
#include "nativestreambuf.h"
#include "helper-types.h"
@@ -81,10 +80,9 @@ class Fcitx {
return uv_run(get_event_base(), UV_RUN_ONCE);
}
- void startup(fcitx::AndroidLibraryDependency dependency,
- const std::function &setupCallback) {
+ void startup(const std::function &setupCallback) {
p_instance = std::make_unique(0, nullptr);
- p_instance->addonManager().registerLoader(std::make_unique(dependency));
+ p_instance->addonManager().registerDefaultLoader(nullptr);
p_dispatcher = std::make_unique();
p_dispatcher->attach(&p_instance->eventLoop());
p_instance->initialize();
@@ -120,10 +118,6 @@ class Fcitx {
return p_frontend->call(idx);
}
- bool forgetCandidate(int idx) {
- return p_frontend->call(idx);
- }
-
bool isInputPanelEmpty() {
return p_frontend->call();
}
@@ -419,6 +413,26 @@ class Fcitx {
return p_frontend->call(offset, limit);
}
+ std::vector getCandidateActions(int idx) {
+ auto actions = std::vector();
+ for (const auto &a: p_frontend->call(idx)) {
+ actions.emplace_back(a);
+ }
+ return actions;
+ }
+
+ void triggerCandidateAction(int idx, int actionIdx) {
+ return p_frontend->call(idx, actionIdx);
+ }
+
+ void setCandidatePagingMode(int mode) {
+ return p_frontend->call(mode);
+ }
+
+ void offsetCandidatePage(int delta) {
+ return p_frontend->call(delta);
+ }
+
void save() {
p_instance->save();
}
@@ -495,9 +509,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
jstring appLib,
jstring extData,
jstring extCache,
- jobjectArray extDomains,
- jobjectArray libraryNames,
- jobjectArray libraryDependencies) {
+ jobjectArray extDomains) {
if (Fcitx::Instance().isRunning()) {
FCITX_ERROR() << "Fcitx is already running!";
return;
@@ -571,21 +583,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
fcitx::registerDomain(CString(env, domain), locale_dir_char);
}
- std::unordered_map> depsMap;
- const int librarySize = env->GetArrayLength(libraryNames);
- for (int i = 0; i < librarySize; i++) {
- auto jstringName = JRef(env, env->GetObjectArrayElement(libraryNames, i));
- auto lib = CString(env, jstringName);
- auto jobjectArrayDeps = JRef(env, env->GetObjectArrayElement(libraryDependencies, i));
- const int depSize = env->GetArrayLength(jobjectArrayDeps);
- std::unordered_set depSet(depSize);
- for (int j = 0; j < depSize; j++) {
- auto jstringDepName = JRef(env, env->GetObjectArrayElement(jobjectArrayDeps, j));
- depSet.emplace(CString(env, jstringDepName));
- }
- depsMap.emplace(lib, depSet);
- }
-
auto candidateListCallback = [](const std::vector &candidates, const int size) {
auto env = GlobalRef->AttachEnv();
auto candidatesArray = JRef(env, env->NewObjectArray(static_cast(candidates.size()), GlobalRef->String, nullptr));
@@ -637,15 +634,17 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 5, *vararg);
};
auto imChangeCallback = []() {
- auto env = GlobalRef->AttachEnv();
- auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
if (!status) return;
+ auto env = GlobalRef->AttachEnv();
+ auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
env->SetObjectArrayElement(vararg, 0, obj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 6, *vararg);
};
auto statusAreaUpdateCallback = []() {
+ std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
+ if (!status) return;
auto env = GlobalRef->AttachEnv();
auto vararg = JRef(env, env->NewObjectArray(static_cast(2), GlobalRef->Object, nullptr));
const auto actions = Fcitx::Instance().statusAreaActions();
@@ -656,7 +655,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(actionArray, i++, obj);
}
env->SetObjectArrayElement(vararg, 0, actionArray);
- std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
auto statusObj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
env->SetObjectArrayElement(vararg, 1, statusObj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 7, *vararg);
@@ -670,15 +668,39 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(vararg, 0, intArray);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 8, *vararg);
};
+ auto pagedCandidateCallback = [](const PagedCandidateEntity &paged) {
+ auto env = GlobalRef->AttachEnv();
+ const int size = static_cast(paged.candidates.size());
+ if (size == 0) {
+ auto vararg = JRef(env, env->NewObjectArray(0, GlobalRef->Object, nullptr));
+ env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 9, *vararg);
+ return;
+ }
+ auto candidatesArray = JRef(env, env->NewObjectArray(size, GlobalRef->Candidate, nullptr));
+ for (int i = 0; i < size; ++i) {
+ env->SetObjectArrayElement(candidatesArray, i, candidateEntityToObject(env, paged.candidates[i]));
+ }
+ auto cursorIndex = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, paged.cursorIndex));
+ auto layoutHint = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, static_cast(paged.layoutHint)));
+ auto hasPrev = JRef(env, env->NewObject(GlobalRef->Boolean, GlobalRef->BooleanInit, paged.hasPrev));
+ auto hasNext = JRef(env, env->NewObject(GlobalRef->Boolean, GlobalRef->BooleanInit, paged.hasNext));
+ auto vararg = JRef(env, env->NewObjectArray(5, GlobalRef->Object, nullptr));
+ env->SetObjectArrayElement(vararg, 0, candidatesArray);
+ env->SetObjectArrayElement(vararg, 1, cursorIndex);
+ env->SetObjectArrayElement(vararg, 2, layoutHint);
+ env->SetObjectArrayElement(vararg, 3, hasPrev);
+ env->SetObjectArrayElement(vararg, 4, hasNext);
+ env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 9, *vararg);
+ };
auto toastCallback = [](const std::string &s) {
auto env = GlobalRef->AttachEnv();
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->ShowToast, *JString(env, s));
};
umask(007);
- fcitx::StandardPath::global().syncUmask();
+ fcitx::StandardPaths::global().syncUmask();
- Fcitx::Instance().startup(depsMap, [&](auto *androidfrontend) {
+ Fcitx::Instance().startup([&](auto *androidfrontend) {
FCITX_INFO() << "Setting up callback";
readyCallback();
androidfrontend->template call(candidateListCallback);
@@ -689,6 +711,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
androidfrontend->template call(imChangeCallback);
androidfrontend->template call(statusAreaUpdateCallback);
androidfrontend->template call(deleteSurroundingCallback);
+ androidfrontend->template call(pagedCandidateCallback);
androidfrontend->template call(toastCallback);
});
FCITX_INFO() << "Finishing startup";
@@ -724,28 +747,31 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_reloadFcitxConfig(JNIEnv *env, jclass c
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxString(JNIEnv *env, jclass clazz, jstring key, jint state, jboolean up, jint timestamp) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxString(JNIEnv *env, jclass clazz, jstring key, jint state, jint code, jboolean up, jint timestamp) {
RETURN_IF_NOT_RUNNING
fcitx::Key parsedKey{fcitx::Key::keySymFromString(CString(env, key)),
- fcitx::KeyStates(static_cast(state))};
+ fcitx::KeyStates(static_cast(state)),
+ code + /* evdev offset */ 8};
Fcitx::Instance().sendKey(parsedKey, up, timestamp);
}
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxChar(JNIEnv *env, jclass clazz, jchar c, jint state, jboolean up, jint timestamp) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxChar(JNIEnv *env, jclass clazz, jchar c, jint state, jint code, jboolean up, jint timestamp) {
RETURN_IF_NOT_RUNNING
const fcitx::Key parsedKey{fcitx::Key::keySymFromString(reinterpret_cast(&c)),
- fcitx::KeyStates(static_cast(state))};
+ fcitx::KeyStates(static_cast(state)),
+ code + /* evdev offset */ 8};
Fcitx::Instance().sendKey(parsedKey, up, timestamp);
}
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeySymToFcitx(JNIEnv *env, jclass clazz, jint sym, jint state, jboolean up, jint timestamp) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeySymToFcitx(JNIEnv *env, jclass clazz, jint sym, jint state, jint code, jboolean up, jint timestamp) {
RETURN_IF_NOT_RUNNING
fcitx::Key key{fcitx::KeySym(static_cast(sym)),
- fcitx::KeyStates(static_cast(state))};
+ fcitx::KeyStates(static_cast(state)),
+ code + /* evdev offset */ 8};
Fcitx::Instance().sendKey(key, up, timestamp);
}
@@ -753,18 +779,9 @@ extern "C"
JNIEXPORT jboolean JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_selectCandidate(JNIEnv *env, jclass clazz, jint idx) {
RETURN_VALUE_IF_NOT_RUNNING(false)
- FCITX_DEBUG() << "selectCandidate: #" << idx;
return Fcitx::Instance().select(idx);
}
-extern "C"
-JNIEXPORT jboolean JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_forgetCandidate(JNIEnv *env, jclass clazz, jint idx) {
- RETURN_VALUE_IF_NOT_RUNNING(false)
- FCITX_DEBUG() << "forgetCandidate: #" << idx;
- return Fcitx::Instance().forgetCandidate(idx);
-}
-
extern "C"
JNIEXPORT jboolean JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_isInputPanelEmpty(JNIEnv *env, jclass clazz) {
@@ -1027,6 +1044,41 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxCandidates(JNIEnv *env, jclass
return array;
}
+extern "C"
+JNIEXPORT jobjectArray JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxCandidateActions(JNIEnv *env, jclass clazz, jint idx) {
+ RETURN_VALUE_IF_NOT_RUNNING(nullptr)
+ auto actions = Fcitx::Instance().getCandidateActions(idx);
+ int size = static_cast(actions.size());
+ jobjectArray array = env->NewObjectArray(size, GlobalRef->CandidateAction, nullptr);
+ for (int i = 0; i < size; i++) {
+ auto obj = JRef(env, fcitxCandidateActionToObject(env, actions[i]));
+ env->SetObjectArrayElement(array, i, obj);
+ }
+ return array;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_triggerFcitxCandidateAction(JNIEnv *env, jclass clazz, jint idx, jint action_idx) {
+ RETURN_IF_NOT_RUNNING
+ Fcitx::Instance().triggerCandidateAction(idx, action_idx);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxCandidatePagingMode(JNIEnv *env, jclass clazz, jint mode) {
+ RETURN_IF_NOT_RUNNING
+ Fcitx::Instance().setCandidatePagingMode(mode);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_offsetFcitxCandidatePage(JNIEnv *env, jclass clazz, jint delta) {
+ RETURN_IF_NOT_RUNNING
+ Fcitx::Instance().offsetCandidatePage(delta);
+}
+
extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_loopOnce(JNIEnv *env, jclass clazz) {
@@ -1120,7 +1172,7 @@ Java_org_fcitx_fcitx5_android_data_table_TableManager_checkTableDictFormat(JNIEn
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_org_fcitx_fcitx5_android_data_pinyin_CustomPhraseManager_load(JNIEnv *env, jclass clazz) {
- auto fp = fcitx::StandardPath::global().open(fcitx::StandardPath::Type::PkgData, "pinyin/customphrase", O_RDONLY);
+ auto fp = fcitx::StandardPaths::global().open(fcitx::StandardPathsType::PkgData, "pinyin/customphrase");
if (fp.fd() < 0) {
FCITX_INFO() << "cannot open pinyin/customphrase";
return nullptr;
@@ -1166,8 +1218,8 @@ Java_org_fcitx_fcitx5_android_data_pinyin_CustomPhraseManager_save(JNIEnv *env,
*CString(env, phraseValue),
static_cast(phraseOrder));
}
- fcitx::StandardPath::global().safeSave(
- fcitx::StandardPath::Type::PkgData, "pinyin/customphrase",
+ fcitx::StandardPaths::global().safeSave(
+ fcitx::StandardPathsType::PkgData, "pinyin/customphrase",
[&](int fd) {
boost::iostreams::stream_buffer
buffer(fd, boost::iostreams::file_descriptor_flags::never_close_handle);
diff --git a/app/src/main/cpp/object-conversion.h b/app/src/main/cpp/object-conversion.h
index 45c2e2306..aa4155095 100644
--- a/app/src/main/cpp/object-conversion.h
+++ b/app/src/main/cpp/object-conversion.h
@@ -161,4 +161,25 @@ jobject fcitxTextToJObject(JNIEnv *env, const fcitx::Text &text) {
return obj;
}
+jobject fcitxCandidateActionToObject(JNIEnv *env, const CandidateActionEntity &act) {
+ auto obj = env->NewObject(GlobalRef->CandidateAction, GlobalRef->CandidateActionInit,
+ act.id,
+ *JString(env, act.text),
+ act.isSeparator,
+ *JString(env, act.icon),
+ act.isCheckable,
+ act.isChecked
+ );
+ return obj;
+}
+
+jobject candidateEntityToObject(JNIEnv *env, const CandidateEntity &c) {
+ auto obj = env->NewObject(GlobalRef->Candidate, GlobalRef->CandidateInit,
+ *JString(env, c.label),
+ *JString(env, c.text),
+ *JString(env, c.comment)
+ );
+ return obj;
+}
+
#endif //FCITX5_ANDROID_OBJECT_CONVERSION_H
diff --git a/app/src/main/cpp/po/de.po b/app/src/main/cpp/po/de.po
index b04353cf8..e9737c8d4 100644
--- a/app/src/main/cpp/po/de.po
+++ b/app/src/main/cpp/po/de.po
@@ -9,7 +9,7 @@ msgstr ""
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
"Last-Translator: Ettore Atalan , 2022\n"
-"Language-Team: German (https://www.transifex.com/fcitx/teams/12005/de/)\n"
+"Language-Team: German (https://app.transifex.com/fcitx/teams/12005/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -28,6 +28,12 @@ msgstr "Worthinweis"
msgid "Enable word hint"
msgstr "Worthinweis aktivieren"
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
+msgid "Disable word hint based on editor attributes"
+msgstr ""
+
msgid "Word hint page size"
msgstr "Seitengröße des Worthinweises"
@@ -36,3 +42,9 @@ msgstr ""
msgid "Insert space between words"
msgstr ""
+
+msgid "Android Toast & Notification"
+msgstr ""
+
+msgid "Hidden Notifications"
+msgstr ""
diff --git a/app/src/main/cpp/po/es.po b/app/src/main/cpp/po/es.po
index 71d03f71f..d78fede55 100644
--- a/app/src/main/cpp/po/es.po
+++ b/app/src/main/cpp/po/es.po
@@ -28,6 +28,12 @@ msgstr ""
msgid "Enable word hint"
msgstr ""
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
+msgid "Disable word hint based on editor attributes"
+msgstr ""
+
msgid "Word hint page size"
msgstr ""
@@ -36,3 +42,9 @@ msgstr ""
msgid "Insert space between words"
msgstr "Insertar espacio entre palabras"
+
+msgid "Android Toast & Notification"
+msgstr ""
+
+msgid "Hidden Notifications"
+msgstr ""
diff --git a/app/src/main/cpp/po/fcitx5-android.pot b/app/src/main/cpp/po/fcitx5-android.pot
index 31396fa35..0efb63065 100644
--- a/app/src/main/cpp/po/fcitx5-android.pot
+++ b/app/src/main/cpp/po/fcitx5-android.pot
@@ -23,6 +23,9 @@ msgstr ""
msgid "Enable word hint"
msgstr ""
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
msgid "Disable word hint based on editor attributes"
msgstr ""
diff --git a/app/src/main/cpp/po/ja.po b/app/src/main/cpp/po/ja.po
index a61405cf3..d612fdf36 100644
--- a/app/src/main/cpp/po/ja.po
+++ b/app/src/main/cpp/po/ja.po
@@ -29,6 +29,9 @@ msgstr "単語ヒント"
msgid "Enable word hint"
msgstr "単語ヒントを有効にする"
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
msgid "Disable word hint based on editor attributes"
msgstr ""
diff --git a/app/src/main/cpp/po/ru.po b/app/src/main/cpp/po/ru.po
index 4120eda0b..9af294da3 100644
--- a/app/src/main/cpp/po/ru.po
+++ b/app/src/main/cpp/po/ru.po
@@ -29,6 +29,9 @@ msgstr "Подсказка слова"
msgid "Enable word hint"
msgstr "Включить подсказку слова"
+msgid "Enable word hint when using physical keyboard"
+msgstr "Включить подсказки слов при использовании физической клавиатуры"
+
msgid "Disable word hint based on editor attributes"
msgstr "Отключить подсказки слов в зависимости от свойств редактора"
diff --git a/app/src/main/cpp/po/zh_CN.po b/app/src/main/cpp/po/zh_CN.po
index c86f46a54..5be82e30c 100644
--- a/app/src/main/cpp/po/zh_CN.po
+++ b/app/src/main/cpp/po/zh_CN.po
@@ -2,6 +2,7 @@
# Translators:
# Potato Hatsue, 2022
# rocka, 2024
+# Yiyu Liu, 2024
#
msgid ""
msgstr ""
@@ -9,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: https://github.com/fcitx5-android/fcitx5-android/issues\n"
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
-"Last-Translator: rocka, 2024\n"
+"Last-Translator: Yiyu Liu, 2024\n"
"Language-Team: Chinese (China) (https://app.transifex.com/fcitx/teams/12005/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +30,9 @@ msgstr "单词提示"
msgid "Enable word hint"
msgstr "启用单词提示"
+msgid "Enable word hint when using physical keyboard"
+msgstr "在使用物理键盘时启用单词提示"
+
msgid "Disable word hint based on editor attributes"
msgstr "根据编辑器属性禁用单词提示"
diff --git a/app/src/main/cpp/po/zh_TW.po b/app/src/main/cpp/po/zh_TW.po
index ad28b50a8..36cd22166 100644
--- a/app/src/main/cpp/po/zh_TW.po
+++ b/app/src/main/cpp/po/zh_TW.po
@@ -2,8 +2,7 @@
# Translators:
# 黃柏諺 , 2022
# Jia-Bin, 2022
-# rocka, 2022
-# Lau YeeYu, 2024
+# Yiyu Liu, 2024
#
msgid ""
msgstr ""
@@ -11,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: https://github.com/fcitx5-android/fcitx5-android/issues\n"
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
-"Last-Translator: Lau YeeYu, 2024\n"
+"Last-Translator: Yiyu Liu, 2024\n"
"Language-Team: Chinese (Taiwan) (https://app.transifex.com/fcitx/teams/12005/zh_TW/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -26,16 +25,19 @@ msgid "Android Keyboard"
msgstr "Android 鍵盤"
msgid "Word hint"
-msgstr "單字提示"
+msgstr "字詞提示"
msgid "Enable word hint"
-msgstr "啟用單字提示"
+msgstr "啟用字詞提示"
+
+msgid "Enable word hint when using physical keyboard"
+msgstr "在使用物理鍵盤時啟用字詞提示"
msgid "Disable word hint based on editor attributes"
msgstr "依據編輯器屬性禁用字詞提示"
msgid "Word hint page size"
-msgstr "單字提示頁大小"
+msgstr "字詞提示頁大小"
msgid "Choose key modifier"
msgstr "選詞修飾鍵"
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
index 31c354292..e52553b36 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
@@ -14,6 +14,7 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Process
import android.util.Log
+import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineName
@@ -59,6 +60,18 @@ class FcitxApplication : Application() {
}
}
+ private val restartFcitxInstanceReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_RESTART_FCITX_INSTANCE) return
+ if (FcitxDaemon.getFirstConnectionOrNull() != null) {
+ Timber.i("Received broadcast '${intent.action}', try to restart fcitx instance ...")
+ FcitxDaemon.restartFcitx()
+ } else {
+ Timber.i("Received broadcast '${intent.action}', but there's no fcitx instance")
+ }
+ }
+ }
+
var isDirectBootMode = false
private set
@@ -140,12 +153,20 @@ class FcitxApplication : Application() {
AppPrefs.getInstance().syncToDeviceEncryptedStorage()
ThemeManager.syncToDeviceEncryptedStorage()
}
+ ContextCompat.registerReceiver(
+ this,
+ restartFcitxInstanceReceiver,
+ IntentFilter(ACTION_RESTART_FCITX_INSTANCE),
+ PERMISSION_TEST_INPUT_METHOD,
+ null,
+ ContextCompat.RECEIVER_EXPORTED
+ )
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- ThemeManager.onSystemDarkModeChange(newConfig.isDarkMode())
- Locales.onLocaleChange(resources.configuration)
+ ThemeManager.onSystemPlatteChange(newConfig)
+ Locales.onLocaleChange(newConfig)
}
companion object {
@@ -156,5 +177,21 @@ class FcitxApplication : Application() {
fun getLastPid() = lastPid
private const val MAX_STACKTRACE_SIZE = 128000
+
+ const val ACTION_RESTART_FCITX_INSTANCE =
+ "${BuildConfig.APPLICATION_ID}.action.RESTART_FCITX_INSTANCE"
+
+ /**
+ * This permission is requested by com.android.shell, makes it possible to restart
+ * fcitx instance from `adb shell am` command:
+ * ```sh
+ * adb shell am broadcast -a org.fcitx.fcitx5.android.action.RESTART_FCITX_INSTANCE
+ * ```
+ * https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.0.0_r1/packages/Shell/AndroidManifest.xml#67
+ *
+ * other candidate: android.permission.TEST_INPUT_METHOD requires Android 14
+ * https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-14.0.0_r1/packages/Shell/AndroidManifest.xml#628
+ */
+ const val PERMISSION_TEST_INPUT_METHOD = "android.permission.READ_INPUT_STATE"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt
index 800ede315..098f34c06 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt
@@ -148,6 +148,7 @@ value class CapabilityFlags constructor(val flags: ULong) {
}
if (equals(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) {
flags += CapabilityFlag.Sensitive
+ flags += CapabilityFlag.NoSpellCheck
}
if (equals(InputType.TYPE_TEXT_VARIATION_URI)) {
flags += CapabilityFlag.Url
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
index 8a16264f8..54aa7a084 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
@@ -73,20 +73,31 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun save() = withFcitxContext { saveFcitxState() }
override suspend fun reloadConfig() = withFcitxContext { reloadFcitxConfig() }
- override suspend fun sendKey(key: String, states: UInt, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeyToFcitxString(key, states.toInt(), up, timestamp) }
-
- override suspend fun sendKey(c: Char, states: UInt, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeyToFcitxChar(c, states.toInt(), up, timestamp) }
-
- override suspend fun sendKey(sym: Int, states: UInt, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeySymToFcitx(sym, states.toInt(), up, timestamp) }
-
- override suspend fun sendKey(sym: KeySym, states: KeyStates, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeySymToFcitx(sym.sym, states.toInt(), up, timestamp) }
+ override suspend fun sendKey(
+ key: String,
+ states: UInt,
+ code: Int,
+ up: Boolean,
+ timestamp: Int
+ ) =
+ withFcitxContext { sendKeyToFcitxString(key, states.toInt(), code, up, timestamp) }
+
+ override suspend fun sendKey(c: Char, states: UInt, code: Int, up: Boolean, timestamp: Int) =
+ withFcitxContext { sendKeyToFcitxChar(c, states.toInt(), code, up, timestamp) }
+
+ override suspend fun sendKey(sym: Int, states: UInt, code: Int, up: Boolean, timestamp: Int) =
+ withFcitxContext { sendKeySymToFcitx(sym, states.toInt(), code, up, timestamp) }
+
+ override suspend fun sendKey(
+ sym: KeySym,
+ states: KeyStates,
+ code: Int,
+ up: Boolean,
+ timestamp: Int
+ ) =
+ withFcitxContext { sendKeySymToFcitx(sym.sym, states.toInt(), code, up, timestamp) }
override suspend fun select(idx: Int): Boolean = withFcitxContext { selectCandidate(idx) }
- override suspend fun forget(idx: Int): Boolean = withFcitxContext { forgetCandidate(idx) }
override suspend fun isEmpty(): Boolean = withFcitxContext { isInputPanelEmpty() }
override suspend fun reset() = withFcitxContext { resetInputContext() }
override suspend fun moveCursor(position: Int) = withFcitxContext { repositionCursor(position) }
@@ -164,6 +175,18 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun getCandidates(offset: Int, limit: Int): Array =
withFcitxContext { getFcitxCandidates(offset, limit) ?: emptyArray() }
+ override suspend fun getCandidateActions(idx: Int): Array =
+ withFcitxContext { getFcitxCandidateActions(idx) ?: emptyArray() }
+
+ override suspend fun triggerCandidateAction(idx: Int, actionIdx: Int) =
+ withFcitxContext { triggerFcitxCandidateAction(idx, actionIdx) }
+
+ override suspend fun setCandidatePagingMode(mode: Int) =
+ withFcitxContext { setFcitxCandidatePagingMode(mode) }
+
+ override suspend fun offsetCandidatePage(delta: Int) =
+ withFcitxContext { offsetFcitxCandidatePage(delta) }
+
init {
if (lifecycle.currentState != FcitxLifecycle.State.STOPPED)
throw IllegalAccessException("Fcitx5 has already been created!")
@@ -209,9 +232,7 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
appLib: String,
extData: String,
extCache: String,
- extDomains: Array,
- libraryNames: Array,
- libraryDependencies: Array>
+ extDomains: Array
)
@JvmStatic
@@ -227,20 +248,23 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
external fun reloadFcitxConfig()
@JvmStatic
- external fun sendKeyToFcitxString(key: String, state: Int, up: Boolean, timestamp: Int)
+ external fun sendKeyToFcitxString(
+ key: String,
+ state: Int,
+ code: Int,
+ up: Boolean,
+ timestamp: Int
+ )
@JvmStatic
- external fun sendKeyToFcitxChar(c: Char, state: Int, up: Boolean, timestamp: Int)
+ external fun sendKeyToFcitxChar(c: Char, state: Int, code: Int, up: Boolean, timestamp: Int)
@JvmStatic
- external fun sendKeySymToFcitx(sym: Int, state: Int, up: Boolean, timestamp: Int)
+ external fun sendKeySymToFcitx(sym: Int, state: Int, code: Int, up: Boolean, timestamp: Int)
@JvmStatic
external fun selectCandidate(idx: Int): Boolean
- @JvmStatic
- external fun forgetCandidate(idx: Int): Boolean
-
@JvmStatic
external fun isInputPanelEmpty(): Boolean
@@ -331,6 +355,18 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
@JvmStatic
external fun getFcitxCandidates(offset: Int, limit: Int): Array?
+ @JvmStatic
+ external fun getFcitxCandidateActions(idx: Int): Array?
+
+ @JvmStatic
+ external fun triggerFcitxCandidateAction(idx: Int, actionIdx: Int)
+
+ @JvmStatic
+ external fun setFcitxCandidatePagingMode(mode: Int)
+
+ @JvmStatic
+ external fun offsetFcitxCandidatePage(delta: Int)
+
@JvmStatic
external fun loopOnce()
@@ -386,18 +422,14 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
val plugins = DataManager.getLoadedPlugins()
val nativeLibDir = StringBuilder(context.applicationInfo.nativeLibraryDir)
val extDomains = arrayListOf()
- val libraryNames = arrayListOf()
- val libraryDependency = arrayListOf>()
plugins.forEach {
- nativeLibDir.append(':')
- nativeLibDir.append(it.nativeLibraryDir)
+ if (it.nativeLibraryDir.isNotBlank()) {
+ nativeLibDir.append(':')
+ nativeLibDir.append(it.nativeLibraryDir)
+ }
it.domain?.let { d ->
extDomains.add(d)
}
- it.libraryDependency.forEach { (lib, dep) ->
- libraryNames.add(lib)
- libraryDependency.add(dep.toTypedArray())
- }
}
Timber.d(
"""
@@ -415,9 +447,7 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
nativeLibDir.toString(),
(getExternalFilesDir(null) ?: filesDir).absolutePath,
(externalCacheDir ?: cacheDir).absolutePath,
- extDomains.toTypedArray(),
- libraryNames.toTypedArray(),
- libraryDependency.toTypedArray()
+ extDomains.toTypedArray()
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
index 16a832c43..b6fd8ce8e 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
@@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.SharedFlow
*/
interface FcitxAPI {
-
enum class AddonDep {
Required,
Optional
@@ -43,16 +42,15 @@ interface FcitxAPI {
suspend fun reloadConfig()
- suspend fun sendKey(key: String, states: UInt = 0u, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(key: String, states: UInt = 0u, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
- suspend fun sendKey(c: Char, states: UInt = 0u, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(c: Char, states: UInt = 0u, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
- suspend fun sendKey(sym: Int, states: UInt = 0u, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(sym: Int, states: UInt = 0u, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
- suspend fun sendKey(sym: KeySym, states: KeyStates, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(sym: KeySym, states: KeyStates, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
suspend fun select(idx: Int): Boolean
- suspend fun forget(idx: Int): Boolean
suspend fun isEmpty(): Boolean
suspend fun reset()
suspend fun moveCursor(position: Int)
@@ -101,4 +99,10 @@ interface FcitxAPI {
suspend fun getCandidates(offset: Int, limit: Int): Array
+ suspend fun getCandidateActions(idx: Int): Array
+ suspend fun triggerCandidateAction(idx: Int, actionIdx: Int)
+
+ suspend fun setCandidatePagingMode(mode: Int)
+ suspend fun offsetCandidatePage(delta: Int)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt
index 33e0025e4..c596c8117 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
@@ -21,23 +21,18 @@ import kotlin.coroutines.CoroutineContext
class FcitxDispatcher(private val controller: FcitxController) : CoroutineDispatcher() {
- class WrappedRunnable(private val runnable: Runnable, private val name: String? = null) :
- Runnable by runnable {
+ class WrappedRunnable(private val runnable: Runnable) : Runnable by runnable {
private val time = System.currentTimeMillis()
- var started = false
- private set
-
- private val delta
- get() = System.currentTimeMillis() - time
override fun run() {
- if (delta > JOB_WAITING_LIMIT)
- Timber.w("${toString()} has waited $delta ms to get run since created!")
- started = true
+ val delta = System.currentTimeMillis() - time
+ if (delta > JOB_WAITING_LIMIT) {
+ Timber.w("$this has waited $delta ms to get run since created!")
+ }
runnable.run()
}
- override fun toString(): String = "WrappedRunnable[${name ?: hashCode()}]"
+ override fun toString(): String = "WrappedRunnable[${hashCode()}]"
}
// this is fcitx main thread
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
index 3172ba6f1..1c119fab2 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
@@ -6,6 +6,8 @@ package org.fcitx.fcitx5.android.core
sealed class FcitxEvent(open val data: T) {
+ data class Candidate(val label: String, val text: String, val comment: String)
+
abstract val eventType: EventType
data class CandidateListEvent(override val data: Data) :
@@ -13,7 +15,7 @@ sealed class FcitxEvent(open val data: T) {
override val eventType = EventType.Candidate
- data class Data(val total: Int, val candidates: Array) {
+ data class Data(val total: Int = -1, val candidates: Array = emptyArray()) {
override fun toString(): String =
"total=$total, candidates=[${candidates.joinToString(limit = 5)}]"
@@ -121,11 +123,63 @@ sealed class FcitxEvent(open val data: T) {
data class DeleteSurroundingEvent(override val data: Data) :
FcitxEvent(data) {
- override val eventType = EventType.DeleteSurrounding
+ override val eventType = EventType.DeleteSurrounding
data class Data(val before: Int, val after: Int)
}
+ data class PagedCandidateEvent(override val data: Data) :
+ FcitxEvent(data) {
+
+ override val eventType = EventType.PagedCandidate
+
+ enum class LayoutHint(value: Int) {
+ NotSet(0), Vertical(1), Horizontal(2);
+
+ companion object {
+ private val Types = entries.toTypedArray()
+ fun of(value: Int) = Types[value]
+ }
+ }
+
+ data class Data(
+ val candidates: Array,
+ val cursorIndex: Int,
+ val layoutHint: LayoutHint,
+ val hasPrev: Boolean,
+ val hasNext: Boolean
+ ) {
+ companion object {
+ @Suppress("BooleanLiteralArgument")
+ val Empty = Data(emptyArray(), -1, LayoutHint.NotSet, false, false)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Data
+
+ if (!candidates.contentEquals(other.candidates)) return false
+ if (cursorIndex != other.cursorIndex) return false
+ if (layoutHint != other.layoutHint) return false
+ if (hasPrev != other.hasPrev) return false
+ if (hasNext != other.hasNext) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = candidates.contentHashCode()
+ result = 31 * result + cursorIndex
+ result = 31 * result + layoutHint.hashCode()
+ result = 31 * result + hasPrev.hashCode()
+ result = 31 * result + hasNext.hashCode()
+ return result
+ }
+ }
+ }
+
data class UnknownEvent(override val data: Array) : FcitxEvent>(data) {
override val eventType = EventType.Unknown
@@ -156,12 +210,13 @@ sealed class FcitxEvent(open val data: T) {
Change,
StatusArea,
DeleteSurrounding,
+ PagedCandidate,
Unknown
}
companion object {
- private val Types = EventType.values()
+ private val Types = EventType.entries.toTypedArray()
@Suppress("UNCHECKED_CAST")
fun create(type: Int, params: Array) =
@@ -206,6 +261,19 @@ sealed class FcitxEvent(open val data: T) {
EventType.DeleteSurrounding -> (params[0] as IntArray).let {
DeleteSurroundingEvent(DeleteSurroundingEvent.Data(it[0], it[1]))
}
+ EventType.PagedCandidate -> if (params.isEmpty()) {
+ PagedCandidateEvent(PagedCandidateEvent.Data.Empty)
+ } else {
+ PagedCandidateEvent(
+ PagedCandidateEvent.Data(
+ params[0] as Array,
+ params[1] as Int,
+ PagedCandidateEvent.LayoutHint.of(params[2] as Int),
+ params[3] as Boolean,
+ params[4] as Boolean
+ )
+ )
+ }
else -> UnknownEvent(params)
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
index 4d4f2da3b..19ab51286 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
@@ -92,6 +92,8 @@ value class KeyStates(val states: UInt) {
companion object {
val Empty = KeyStates(0u)
+ val Virtual = KeyStates(KeyState.Virtual)
+
fun of(v: Int) = KeyStates(v.toUInt())
fun fromKeyEvent(event: KeyEvent): KeyStates {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt
index 00706ed2a..a07d60cbe 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt
@@ -40,7 +40,6 @@ object SubtypeManager {
.setSubtypeId(im.uniqueName.hashCode())
.setSubtypeExtraValue(im.uniqueName)
.setSubtypeNameOverride(im.displayName)
- .setSubtypeLocale(im.languageCode)
.setSubtypeMode(MODE_KEYBOARD)
.setIsAsciiCapable(im.uniqueName == IM_KEYBOARD)
.build()
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt
index ac863b111..12911945b 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt
@@ -1,11 +1,12 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
data class InputMethodSubMode(val name: String, val label: String, val icon: String) {
constructor() : this("", "", "")
@@ -74,6 +75,7 @@ data class InputMethodEntry(
}
@Parcelize
+@Serializable
data class RawConfig(
val name: String,
val comment: String,
@@ -158,6 +160,7 @@ data class AddonInfo(
val dependencies: Array = arrayOf(),
val optionalDependencies: Array = arrayOf(),
) {
+ @Suppress("UNUSED") // used in JNI
constructor(
uniqueName: String,
name: String,
@@ -260,3 +263,12 @@ data class Action(
return result
}
}
+
+data class CandidateAction(
+ val id: Int,
+ val text: String,
+ val isSeparator: Boolean,
+ val icon: String,
+ val isCheckable: Boolean,
+ val isChecked: Boolean
+)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt
index fd19acf2c..f134cc767 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core.data
@@ -29,6 +29,11 @@ import kotlin.concurrent.withLock
*/
object DataManager {
+ data class PluginSet(
+ val loaded: Set,
+ val failed: Map
+ )
+
const val PLUGIN_INTENT = "${BuildConfig.APPLICATION_ID}.plugin.MANIFEST"
private val lock = ReentrantLock()
@@ -69,6 +74,8 @@ object DataManager {
fun getLoadedPlugins(): Set = loadedPlugins
fun getFailedPlugins(): Map = failedPlugins
+ fun getSyncedPluginSet() = PluginSet(loadedPlugins, failedPlugins)
+
/**
* Will be cleared after each sync
*/
@@ -77,7 +84,7 @@ object DataManager {
fun addOnNextSyncedCallback(block: () -> Unit) =
callbacks.add(block)
- fun detectPlugins(): Pair, Map> {
+ fun detectPlugins(): PluginSet {
val toLoad = mutableSetOf()
val preloadFailed = mutableMapOf()
@@ -113,36 +120,15 @@ object DataManager {
var apiVersion: String? = null
var description: String? = null
var hasService = false
- val libraryDependency = mutableMapOf>()
- var library: String? = null
- var dependency: ArrayList? = null
var text: String? = null
while ((eventType != XmlPullParser.END_DOCUMENT)) {
when (eventType) {
XmlPullParser.TEXT -> text = parser.text
- XmlPullParser.START_TAG -> when (parser.name) {
- "library" -> {
- dependency = arrayListOf()
- for (i in 0.. when (parser.name) {
"apiVersion" -> apiVersion = text
"domain" -> domain = text
"description" -> description = text
"hasService" -> hasService = text?.lowercase() == "true"
- "dependency" -> dependency?.add(text!!)
- "library" -> {
- if (library != null && dependency != null) {
- libraryDependency[library] = dependency
- library = null
- dependency = null
- }
- }
}
}
eventType = parser.next()
@@ -176,9 +162,8 @@ object DataManager {
domain,
description,
hasService,
- info.versionName,
- info.applicationInfo.nativeLibraryDir,
- libraryDependency
+ info.versionName ?: "",
+ info.applicationInfo?.nativeLibraryDir ?: ""
)
)
} else {
@@ -190,7 +175,7 @@ object DataManager {
preloadFailed[packageName] = PluginLoadFailed.PluginDescriptorParseError
}
}
- return toLoad to preloadFailed
+ return PluginSet(toLoad, preloadFailed)
}
fun sync() = lock.withLock {
@@ -321,4 +306,4 @@ object DataManager {
sync()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt
index 4583e5886..cec9078ec 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core.data
@@ -32,8 +32,7 @@ data class PluginDescriptor(
*/
val hasService: Boolean,
val versionName: String,
- val nativeLibraryDir: String,
- val libraryDependency: Map>
+ val nativeLibraryDir: String
) {
val name = packageName.removePrefix(pluginPackagePrefix).removeSuffix(pluginPackageSuffix)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt
index f9d71acc6..fef6d17b1 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt
@@ -10,56 +10,56 @@ import android.os.VibrationEffect
import android.provider.Settings
import android.view.HapticFeedbackConstants
import android.view.View
+import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
-import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
+import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.audioManager
-import org.fcitx.fcitx5.android.utils.isSystemSettingEnabled
+import org.fcitx.fcitx5.android.utils.getSystemSettings
import org.fcitx.fcitx5.android.utils.vibrator
object InputFeedbacks {
- enum class InputFeedbackMode {
- Enabled, Disabled, FollowingSystem;
-
- companion object : ManagedPreference.StringLikeCodec {
- override fun decode(raw: String) = InputFeedbackMode.valueOf(raw)
- }
+ enum class InputFeedbackMode(override val stringRes: Int) : ManagedPreferenceEnum {
+ FollowingSystem(R.string.following_system_settings),
+ Enabled(R.string.enabled),
+ Disabled(R.string.disabled);
}
private var systemSoundEffects = false
private var systemHapticFeedback = false
fun syncSystemPrefs() {
- systemSoundEffects = isSystemSettingEnabled(Settings.System.SOUND_EFFECTS_ENABLED)
+ systemSoundEffects = getSystemSettings(Settings.System.SOUND_EFFECTS_ENABLED) == 1
// it says "Replaced by using android.os.VibrationAttributes.USAGE_TOUCH"
// but gives no clue about how to use it, and this one still works
@Suppress("DEPRECATION")
- systemHapticFeedback = isSystemSettingEnabled(Settings.System.HAPTIC_FEEDBACK_ENABLED)
+ systemHapticFeedback = getSystemSettings(Settings.System.HAPTIC_FEEDBACK_ENABLED) == 1
}
- private val soundOnKeyPress by AppPrefs.getInstance().keyboard.soundOnKeyPress
- private val soundOnKeyPressVolume by AppPrefs.getInstance().keyboard.soundOnKeyPressVolume
- private val hapticOnKeyPress by AppPrefs.getInstance().keyboard.hapticOnKeyPress
- private val buttonPressVibrationMilliseconds by AppPrefs.getInstance().keyboard.buttonPressVibrationMilliseconds
- private val buttonLongPressVibrationMilliseconds by AppPrefs.getInstance().keyboard.buttonLongPressVibrationMilliseconds
- private val buttonPressVibrationAmplitude by AppPrefs.getInstance().keyboard.buttonPressVibrationAmplitude
- private val buttonLongPressVibrationAmplitude by AppPrefs.getInstance().keyboard.buttonLongPressVibrationAmplitude
+ private val keyboardPrefs = AppPrefs.getInstance().keyboard
+
+ private val soundOnKeyPress by keyboardPrefs.soundOnKeyPress
+ private val soundOnKeyPressVolume by keyboardPrefs.soundOnKeyPressVolume
+ private val hapticOnKeyPress by keyboardPrefs.hapticOnKeyPress
+ private val hapticOnKeyUp by keyboardPrefs.hapticOnKeyUp
+ private val buttonPressVibrationMilliseconds by keyboardPrefs.buttonPressVibrationMilliseconds
+ private val buttonLongPressVibrationMilliseconds by keyboardPrefs.buttonLongPressVibrationMilliseconds
+ private val buttonPressVibrationAmplitude by keyboardPrefs.buttonPressVibrationAmplitude
+ private val buttonLongPressVibrationAmplitude by keyboardPrefs.buttonLongPressVibrationAmplitude
private val vibrator = appContext.vibrator
private val hasAmplitudeControl =
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && vibrator.hasAmplitudeControl()
- private val audioManager = appContext.audioManager
-
- fun hapticFeedback(view: View, longPress: Boolean = false) {
+ fun hapticFeedback(view: View, longPress: Boolean = false, keyUp: Boolean = false) {
when (hapticOnKeyPress) {
InputFeedbackMode.Enabled -> {}
InputFeedbackMode.Disabled -> return
InputFeedbackMode.FollowingSystem -> if (!systemHapticFeedback) return
}
-
+ if (keyUp && !hapticOnKeyUp) return
val duration: Long
val amplitude: Int
val hfc: Int
@@ -70,25 +70,36 @@ object InputFeedbacks {
} else {
duration = buttonPressVibrationMilliseconds.toLong()
amplitude = buttonPressVibrationAmplitude
- hfc = HapticFeedbackConstants.KEYBOARD_TAP
+ hfc = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && keyUp) {
+ HapticFeedbackConstants.KEYBOARD_RELEASE
+ } else {
+ HapticFeedbackConstants.KEYBOARD_TAP
+ }
}
- val useVibrator = duration != 0L
- if (useVibrator) {
+ // there is `VibrationEffect.DEFAULT_AMPLITUDE` but no default duration;
+ // also `VibrationEffect.createOneShot()` only accepts positive duration.
+ // so changing amplitude without changing duration makes no sense
+ if (duration != 0L) {
// on Android 13, if system haptic feedback was disabled, `vibrator.vibrate()` won't work
// but `view.performHapticFeedback()` with `FLAG_IGNORE_GLOBAL_SETTING` still works
if (hasAmplitudeControl && amplitude != 0) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val ve = VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE)
+ vibrator.vibrate(ve)
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
}
} else {
- // it says "Starting TIRAMISU only privileged apps can ignore user settings for touch feedback"
- // but we still seem to be able to use `FLAG_IGNORE_GLOBAL_SETTING`
- @Suppress("DEPRECATION")
- val flags =
- HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING or HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ var flags = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+ if (hapticOnKeyPress == InputFeedbackMode.Enabled) {
+ // it says "Starting TIRAMISU only privileged apps can ignore user settings for touch feedback"
+ // but we still seem to be able to use `FLAG_IGNORE_GLOBAL_SETTING`
+ @Suppress("DEPRECATION")
+ flags = flags or HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ }
view.performHapticFeedback(hfc, flags)
}
}
@@ -97,6 +108,8 @@ object InputFeedbacks {
Standard, SpaceBar, Delete, Return
}
+ private val audioManager = appContext.audioManager
+
fun soundEffect(effect: SoundEffect) {
when (soundOnKeyPress) {
InputFeedbackMode.Enabled -> {}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
index 9afc698aa..deeadf0db 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
@@ -1,43 +1,76 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
+
package org.fcitx.fcitx5.android.data
+import android.content.Context
+import androidx.core.content.edit
+import kotlinx.serialization.json.Json
import org.fcitx.fcitx5.android.FcitxApplication
+import timber.log.Timber
-// Not thread-safe
-class RecentlyUsed(
- val fileName: String,
- val capacity: Int
-) : LinkedHashMap(0, .75f, true) {
+class RecentlyUsed(val type: String, val limit: Int) {
companion object {
+ // for backwords compatibility only
const val DIR_NAME = "recently_used"
+ const val PREFERENCE_NAME = "picker_recently_used"
}
- private val file =
- FcitxApplication.getInstance().directBootAwareContext.filesDir.resolve(DIR_NAME).run {
- mkdirs()
- resolve(fileName).apply { createNewFile() }
- }
+ private val sharedPreferences = FcitxApplication.getInstance().directBootAwareContext
+ .getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
- fun load() {
- val xs = file.readLines()
- xs.forEach {
- if (it.isNotBlank())
- put(it, it)
- }
+ private val map = LinkedHashMap(limit).apply {
+ (migrate() ?: load()).forEach { put(it, true) }
}
- fun save() {
- file.writeText(values.joinToString("\n"))
+ val items: List get() = map.keys.reversed()
+
+ private fun load(): List {
+ val rawValue = sharedPreferences.getString(type, "") ?: ""
+ if (rawValue.isEmpty()) {
+ return emptyList()
+ }
+ return try {
+ Json.decodeFromString>(rawValue)
+ } catch (_: Exception) {
+ sharedPreferences.edit {
+ remove(type)
+ }
+ emptyList()
+ }
}
- override fun removeEldestEntry(eldest: MutableMap.MutableEntry?) =
- size > capacity
+ private fun save() {
+ sharedPreferences.edit {
+ putString(type, Json.encodeToString>(map.keys.toList()))
+ }
+ }
- fun insert(s: String) = put(s, s)
+ fun insert(item: String) {
+ map.put(item, true)
+ save()
+ }
- fun toOrderedList() = values.toList().reversed()
+ fun migrate(): List? {
+ val dir = FcitxApplication.getInstance().directBootAwareContext.filesDir.resolve(DIR_NAME)
+ val file = dir.resolve(type)
+ if (file.exists()) {
+ try {
+ val lines = file.readLines()
+ file.delete()
+ if (dir.list()?.isEmpty() == true) {
+ dir.delete()
+ }
+ return lines
+ } catch (e: Exception) {
+ Timber.w("Failed to migrate RecentlyUsed(type=$type)")
+ Timber.w(e)
+ return null
+ }
+ }
+ return null
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt
index c0c65ec39..39a31ab64 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.data
@@ -14,6 +14,7 @@ import org.fcitx.fcitx5.android.utils.Const
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.errorRuntime
import org.fcitx.fcitx5.android.utils.extract
+import org.fcitx.fcitx5.android.utils.versionCodeCompat
import org.fcitx.fcitx5.android.utils.withTempDir
import timber.log.Timber
import java.io.File
@@ -30,7 +31,7 @@ object UserDataManager {
@Serializable
data class Metadata(
val packageName: String,
- val versionCode: Int,
+ val versionCode: Long,
val versionName: String,
val exportTime: Long
)
@@ -64,13 +65,13 @@ object UserDataManager {
writeFileTree(dataBasesDir, "databases", zipStream)
// external
writeFileTree(externalDir, "external", zipStream)
- // recently_used
- writeFileTree(recentlyUsedDir, "recently_used", zipStream)
+ // recently_used moved to SharedPreference and shoud not be exported
// metadata
zipStream.putNextEntry(ZipEntry("metadata.json"))
+ val pkgInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
val metadata = Metadata(
- BuildConfig.APPLICATION_ID,
- BuildConfig.VERSION_CODE,
+ pkgInfo.packageName,
+ pkgInfo.versionCodeCompat,
Const.versionName,
timestamp
)
@@ -85,7 +86,6 @@ object UserDataManager {
if (exists && isDir) {
source.copyRecursively(target, overwrite = true)
} else {
- source.toString()
Timber.w("Cannot import user data: path='${source.path}', exists=$exists, isDir=$isDir")
}
}
@@ -102,6 +102,7 @@ object UserDataManager {
copyDir(File(tempDir, "shared_prefs"), sharedPrefsDir)
copyDir(File(tempDir, "databases"), dataBasesDir)
copyDir(File(tempDir, "external"), externalDir)
+ // keep importing recently_used for backwords compatibility
copyDir(File(tempDir, "recently_used"), recentlyUsedDir)
metadata
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt
index f669fa51e..92bc85285 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt
@@ -6,8 +6,10 @@ package org.fcitx.fcitx5.android.data.clipboard
import android.content.ClipboardManager
import android.content.Context
+import android.os.Build
import androidx.annotation.Keep
import androidx.room.Room
+import androidx.room.withTransaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -23,6 +25,7 @@ import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
import org.fcitx.fcitx5.android.utils.WeakHashSet
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.clipboardManager
+import timber.log.Timber
object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener,
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
@@ -85,7 +88,7 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener,
clbDb = Room
.databaseBuilder(context, ClipboardDatabase::class.java, "clbdb")
// allow wipe the database instead of crashing when downgrade
- .fallbackToDestructiveMigrationOnDowngrade()
+ .fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true)
.build()
clbDao = clbDb.clipboardDao()
enabledListener.onChange(enabledPref.key, enabledPref.getValue())
@@ -144,26 +147,50 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener,
}
}
+ private var lastClipTimestamp = -1L
+ private var lastClipHash = 0
+
override fun onPrimaryClipChanged() {
- clipboardManager.primaryClip
- ?.let { ClipboardEntry.fromClipData(it, transformer) }
- ?.takeIf { it.text.isNotBlank() }
- ?.let { e ->
- launch {
- mutex.withLock {
- clbDao.find(e.text, e.sensitive)?.let {
- updateLastEntry(it.copy(timestamp = e.timestamp))
- clbDao.updateTime(it.id, e.timestamp)
- return@launch
- }
- val rowId = clbDao.insert(e)
+ val clip = clipboardManager.primaryClip ?: return
+ /**
+ * skip duplicate ClipData
+ * https://developer.android.com/reference/android/content/ClipboardManager.OnPrimaryClipChangedListener#onPrimaryClipChanged()
+ */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val timestamp = clip.description.timestamp
+ if (timestamp == lastClipTimestamp) return
+ lastClipTimestamp = timestamp
+ } else {
+ val timestamp = System.currentTimeMillis()
+ val hash = clip.hashCode()
+ if (timestamp - lastClipTimestamp < 100L && hash == lastClipHash) return
+ lastClipTimestamp = timestamp
+ lastClipHash = hash
+ }
+ launch {
+ mutex.withLock {
+ val entry = ClipboardEntry.fromClipData(clip, transformer) ?: return@withLock
+ if (entry.text.isBlank()) return@withLock
+ try {
+ clbDao.find(entry.text, entry.sensitive)?.let {
+ updateLastEntry(it.copy(timestamp = entry.timestamp))
+ clbDao.updateTime(it.id, entry.timestamp)
+ return@withLock
+ }
+ val insertedEntry = clbDb.withTransaction {
+ val rowId = clbDao.insert(entry)
removeOutdated()
- updateItemCount()
// new entry can be deleted immediately if clipboard limit == 0
- updateLastEntry(clbDao.get(rowId) ?: e)
+ clbDao.get(rowId) ?: entry
}
+ updateLastEntry(insertedEntry)
+ updateItemCount()
+ } catch (exception: Exception) {
+ Timber.w("Failed to update clipboard database: $exception")
+ updateLastEntry(entry)
}
}
+ }
}
private suspend fun removeOutdated() {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt
index bd7cea896..c98252b75 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt
@@ -10,6 +10,7 @@ import android.os.Build
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
+import org.fcitx.fcitx5.android.utils.timestamp
@Entity(tableName = ClipboardEntry.TABLE_NAME)
data class ClipboardEntry(
@@ -52,6 +53,7 @@ data class ClipboardEntry(
}
return ClipboardEntry(
text = if (transformer != null) transformer(str) else str,
+ timestamp = clipData.timestamp(),
type = desc.getMimeType(0),
sensitive = sensitive
)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt
index 4a3fe9997..d84a0215b 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt
@@ -1,22 +1,26 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.data.prefs
import android.content.SharedPreferences
import android.os.Build
+import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.InputFeedbacks.InputFeedbackMode
-import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateMode
import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle
+import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesMode
+import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesOrientation
+import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateMode
import org.fcitx.fcitx5.android.input.keyboard.LangSwitchBehavior
import org.fcitx.fcitx5.android.input.keyboard.SpaceLongPressBehavior
import org.fcitx.fcitx5.android.input.keyboard.SwipeSymbolDirection
import org.fcitx.fcitx5.android.input.picker.PickerWindow
+import org.fcitx.fcitx5.android.input.popup.EmojiModifier
import org.fcitx.fcitx5.android.utils.DeviceUtil
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.vibrator
@@ -34,7 +38,7 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
}
inner class Advanced : ManagedPreferenceCategory(R.string.advanced, sharedPreferences) {
- val ignoreSystemCursor = switch(R.string.ignore_sys_cursor, "ignore_system_cursor", true)
+ val ignoreSystemCursor = switch(R.string.ignore_sys_cursor, "ignore_system_cursor", false)
val hideKeyConfig = switch(R.string.hide_key_config, "hide_key_config", true)
val disableAnimation = switch(R.string.disable_animation, "disable_animation", false)
val vivoKeypressWorkaround = switch(
@@ -42,26 +46,25 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
"vivo_keypress_workaround",
DeviceUtil.isVivoOriginOS
)
+ val ignoreSystemWindowInsets = switch(
+ R.string.ignore_system_window_insets, "ignore_system_window_insets", false
+ )
}
- inner class Keyboard : ManagedPreferenceCategory(R.string.keyboard, sharedPreferences) {
+ inner class Keyboard : ManagedPreferenceCategory(R.string.virtual_keyboard, sharedPreferences) {
val hapticOnKeyPress =
- list(
+ enumList(
R.string.button_haptic_feedback,
"haptic_on_keypress",
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode,
- listOf(
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode.Enabled,
- InputFeedbackMode.Disabled
- ),
- listOf(
- R.string.following_system_settings,
- R.string.enabled,
- R.string.disabled
- )
+ InputFeedbackMode.FollowingSystem
)
+ val hapticOnKeyUp = switch(
+ R.string.button_up_haptic_feedback,
+ "haptic_on_keyup",
+ false
+ ) { hapticOnKeyPress.getValue() != InputFeedbackMode.Disabled }
+ val hapticOnRepeat = switch(R.string.haptic_on_repeat, "haptic_on_repeat", false)
+
val buttonPressVibrationMilliseconds: ManagedPreference.PInt
val buttonLongPressVibrationMilliseconds: ManagedPreference.PInt
@@ -108,21 +111,10 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
buttonLongPressVibrationAmplitude = secondary
}
- val soundOnKeyPress = list(
+ val soundOnKeyPress = enumList(
R.string.button_sound,
"sound_on_keypress",
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode,
- listOf(
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode.Enabled,
- InputFeedbackMode.Disabled
- ),
- listOf(
- R.string.following_system_settings,
- R.string.enabled,
- R.string.disabled
- )
+ InputFeedbackMode.FollowingSystem
)
val soundOnKeyPressVolume = int(
R.string.button_sound_volume,
@@ -152,21 +144,10 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
switch(R.string.show_voice_input_button, "show_voice_input_button", false)
val expandKeypressArea =
switch(R.string.expand_keypress_area, "expand_keypress_area", false)
- val swipeSymbolDirection = list(
+ val swipeSymbolDirection = enumList(
R.string.swipe_symbol_behavior,
"swipe_symbol_behavior",
- SwipeSymbolDirection.Down,
- SwipeSymbolDirection,
- listOf(
- SwipeSymbolDirection.Up,
- SwipeSymbolDirection.Down,
- SwipeSymbolDirection.Disabled
- ),
- listOf(
- R.string.swipe_up,
- R.string.swipe_down,
- R.string.disabled
- )
+ SwipeSymbolDirection.Down
)
val longPressDelay = int(
R.string.keyboard_long_press_delay,
@@ -177,43 +158,19 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
"ms",
10
)
- val spaceKeyLongPressBehavior = list(
+ val spaceKeyLongPressBehavior = enumList(
R.string.space_long_press_behavior,
"space_long_press_behavior",
- SpaceLongPressBehavior.None,
- SpaceLongPressBehavior,
- listOf(
- SpaceLongPressBehavior.None,
- SpaceLongPressBehavior.Enumerate,
- SpaceLongPressBehavior.ToggleActivate,
- SpaceLongPressBehavior.ShowPicker
- ),
- listOf(
- R.string.space_behavior_none,
- R.string.space_behavior_enumerate,
- R.string.space_behavior_activate,
- R.string.space_behavior_picker
- )
+ SpaceLongPressBehavior.None
)
val spaceSwipeMoveCursor =
switch(R.string.space_swipe_move_cursor, "space_swipe_move_cursor", true)
val showLangSwitchKey =
switch(R.string.show_lang_switch_key, "show_lang_switch_key", true)
- val langSwitchKeyBehavior = list(
+ val langSwitchKeyBehavior = enumList(
R.string.lang_switch_key_behavior,
"lang_switch_key_behavior",
- LangSwitchBehavior.Enumerate,
- LangSwitchBehavior,
- listOf(
- LangSwitchBehavior.Enumerate,
- LangSwitchBehavior.ToggleActivate,
- LangSwitchBehavior.NextInputMethodApp
- ),
- listOf(
- R.string.space_behavior_enumerate,
- R.string.space_behavior_activate,
- R.string.lang_switch_behavior_next_ime_app
- )
+ LangSwitchBehavior.Enumerate
) { showLangSwitchKey.getValue() }
val keyboardHeightPercent: ManagedPreference.PInt
@@ -276,35 +233,15 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
keyboardBottomPaddingLandscape = secondary
}
- val horizontalCandidateStyle = list(
+ val horizontalCandidateStyle = enumList(
R.string.horizontal_candidate_style,
"horizontal_candidate_style",
- HorizontalCandidateMode.AutoFillWidth,
- HorizontalCandidateMode,
- listOf(
- HorizontalCandidateMode.NeverFillWidth,
- HorizontalCandidateMode.AutoFillWidth,
- HorizontalCandidateMode.AlwaysFillWidth,
- ),
- listOf(
- R.string.horizontal_candidate_never_fill,
- R.string.horizontal_candidate_auto_fill,
- R.string.horizontal_candidate_always_fill
- )
+ HorizontalCandidateMode.AutoFillWidth
)
- val expandedCandidateStyle = list(
+ val expandedCandidateStyle = enumList(
R.string.expanded_candidate_style,
"expanded_candidate_style",
- ExpandedCandidateStyle.Grid,
- ExpandedCandidateStyle,
- listOf(
- ExpandedCandidateStyle.Grid,
- ExpandedCandidateStyle.Flexbox
- ),
- listOf(
- R.string.expanded_candidate_style_grid,
- R.string.expanded_candidate_style_flexbox
- )
+ ExpandedCandidateStyle.Grid
)
val expandedCandidateGridSpanCount: ManagedPreference.PInt
@@ -328,6 +265,60 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
}
+ inner class Candidates :
+ ManagedPreferenceCategory(R.string.candidates_window, sharedPreferences) {
+ val mode = enumList(
+ R.string.show_candidates_window,
+ "show_candidates_window",
+ FloatingCandidatesMode.InputDevice
+ )
+
+ val orientation = enumList(
+ R.string.candidates_orientation,
+ "candidates_window_orientation",
+ FloatingCandidatesOrientation.Automatic
+ )
+
+ val windowMinWidth = int(
+ R.string.candidates_window_min_width,
+ "candidates_window_min_width",
+ 0,
+ 0,
+ 640,
+ "dp",
+ 10
+ )
+
+ val windowPadding =
+ int(R.string.candidates_window_padding, "candidates_window_padding", 4, 0, 32, "dp")
+
+ val fontSize =
+ int(R.string.candidates_font_size, "candidates_window_font_size", 20, 4, 64, "sp")
+
+ val windowRadius =
+ int(R.string.candidates_window_radius, "candidates_window_radius", 0, 0, 48, "dp")
+
+ val itemPaddingVertical: ManagedPreference.PInt
+ val itemPaddingHorizontal: ManagedPreference.PInt
+
+ init {
+ val (primary, secondary) = twinInt(
+ R.string.candidates_padding,
+ R.string.vertical,
+ "candidates_item_padding_vertical",
+ 2,
+ R.string.horizontal,
+ "candidates_item_padding_horizontal",
+ 4,
+ 0,
+ 64,
+ "dp"
+ )
+ itemPaddingVertical = primary
+ itemPaddingHorizontal = secondary
+ }
+ }
+
inner class Clipboard : ManagedPreferenceCategory(R.string.clipboard, sharedPreferences) {
val clipboardListening = switch(R.string.clipboard_listening, "clipboard_enable", true)
val clipboardHistoryLimit = int(
@@ -354,6 +345,14 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
) { clipboardListening.getValue() }
}
+ inner class Symbols : ManagedPreferenceCategory(R.string.emoji_and_symbols, sharedPreferences) {
+ val defaultEmojiSkinTone = enumList(
+ R.string.default_emoji_skin_tone,
+ "default_emoji_skin_tone",
+ EmojiModifier.SkinTone.Default,
+ )
+ }
+
private val providers = mutableListOf()
fun registerProvider(
@@ -370,13 +369,17 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
val internal = Internal().register()
val keyboard = Keyboard().register()
+ val candidates = Candidates().register()
val clipboard = Clipboard().register()
+ val symbols = Symbols().register()
val advanced = Advanced().register()
+ @Keep
private val onSharedPreferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key == null) return@OnSharedPreferenceChangeListener
providers.forEach {
- it.managedPreferences[key]?.fireChange()
+ it.fireChange(key)
}
}
@@ -394,11 +397,14 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
).forEach {
it.putValueTo(this@edit)
}
- keyboard.managedPreferences.forEach {
- it.value.putValueTo(this@edit)
- }
- clipboard.managedPreferences.forEach {
- it.value.putValueTo(this@edit)
+ listOf(
+ keyboard,
+ candidates,
+ clipboard
+ ).forEach { category ->
+ category.managedPreferences.forEach {
+ it.value.putValueTo(this@edit)
+ }
}
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt
index 6bc725a7e..ca8367b0c 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt
@@ -35,9 +35,7 @@ abstract class ManagedPreference(
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = setValue(value)
- private val listeners by lazy {
- WeakHashSet>()
- }
+ private lateinit var listeners: MutableSet>
/**
* **WARN:** No anonymous listeners, please **KEEP** the reference!
@@ -47,15 +45,19 @@ abstract class ManagedPreference(
* or simply mark the listener with [@Keep][androidx.annotation.Keep] .
*/
fun registerOnChangeListener(listener: OnChangeListener) {
+ if (!::listeners.isInitialized) {
+ listeners = WeakHashSet()
+ }
listeners.add(listener)
}
fun unregisterOnChangeListener(listener: OnChangeListener) {
+ if (!::listeners.isInitialized || listeners.isEmpty()) return
listeners.remove(listener)
}
fun fireChange() {
- if (listeners.isEmpty()) return
+ if (!::listeners.isInitialized || listeners.isEmpty()) return
val newValue = getValue()
listeners.forEach { it.onChange(key, newValue) }
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt
index 8cd5fdaa2..1c8a84994 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt
@@ -49,6 +49,21 @@ abstract class ManagedPreferenceCategory(
return pref
}
+ protected inline fun enumList(
+ @StringRes
+ title: Int,
+ key: String,
+ defaultValue: T,
+ noinline enableUiOn: (() -> Boolean)? = null
+ ): ManagedPreference.PStringLike where T : Enum, T : ManagedPreferenceEnum {
+ val codec = object : ManagedPreference.StringLikeCodec {
+ override fun decode(raw: String): T = enumValueOf(raw)
+ }
+ val entryValues = enumValues().toList()
+ val entryLabels = entryValues.map { it.stringRes }
+ return list(title, key, defaultValue, codec, entryValues, entryLabels, enableUiOn)
+ }
+
protected fun int(
@StringRes
title: Int,
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceEnum.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceEnum.kt
new file mode 100644
index 000000000..a67e8407a
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceEnum.kt
@@ -0,0 +1,13 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.data.prefs
+
+import androidx.annotation.StringRes
+
+interface ManagedPreferenceEnum {
+ @get:StringRes
+ val stringRes: Int
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceProvider.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceProvider.kt
index 1cf246375..449bdc785 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceProvider.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceProvider.kt
@@ -1,13 +1,19 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
+
package org.fcitx.fcitx5.android.data.prefs
import androidx.preference.PreferenceScreen
+import org.fcitx.fcitx5.android.utils.WeakHashSet
abstract class ManagedPreferenceProvider {
+ fun interface OnChangeListener {
+ fun onChange(key: String)
+ }
+
private val _managedPreferences: MutableMap> = mutableMapOf()
private val _managedPreferencesUi: MutableList> = mutableListOf()
@@ -22,6 +28,22 @@ abstract class ManagedPreferenceProvider {
}
+ private val onChangeListeners = WeakHashSet()
+
+ fun registerOnChangeListener(listener: OnChangeListener) {
+ onChangeListeners.add(listener)
+ }
+
+ fun unregisterOnChangeListener(listener: OnChangeListener) {
+ onChangeListeners.remove(listener)
+ }
+
+ fun fireChange(key: String) {
+ val preference = _managedPreferences[key] ?: return
+ onChangeListeners.forEach { it.onChange(key) }
+ preference.fireChange()
+ }
+
fun ManagedPreferenceUi<*>.registerUi() {
_managedPreferencesUi.add(this)
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceVisibilityEvaluator.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceVisibilityEvaluator.kt
index bd62f129d..a4d2bbe03 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceVisibilityEvaluator.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceVisibilityEvaluator.kt
@@ -15,14 +15,12 @@ class ManagedPreferenceVisibilityEvaluator(
// it would be better to declare the dependency relationship, rather than reevaluating on each value changed
@Keep
- private val onValueChangeListener = ManagedPreference.OnChangeListener { _, _ ->
+ private val onValueChangeListener = ManagedPreferenceProvider.OnChangeListener {
evaluateVisibility()
}
init {
- provider.managedPreferences.forEach { (_, pref) ->
- pref.registerOnChangeListener(onValueChangeListener)
- }
+ provider.registerOnChangeListener(onValueChangeListener)
}
fun evaluateVisibility() {
@@ -40,9 +38,7 @@ class ManagedPreferenceVisibilityEvaluator(
}
fun destroy() {
- provider.managedPreferences.forEach { (_, pref) ->
- pref.unregisterOnChangeListener(onValueChangeListener)
- }
+ provider.unregisterOnChangeListener(onValueChangeListener)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/BuiltinQuickPhrase.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/BuiltinQuickPhrase.kt
index 02a1e9a33..b3cd68eea 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/BuiltinQuickPhrase.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/BuiltinQuickPhrase.kt
@@ -14,18 +14,13 @@ class BuiltinQuickPhrase(
init {
ensureFileExists()
+ evaluateOverride()
}
- var override: CustomQuickPhrase? =
- if (overrideFile.exists())
- CustomQuickPhrase(overrideFile)
- else {
- val disabledOverride = File(overrideFile.path + ".$DISABLE")
- if (disabledOverride.exists())
- CustomQuickPhrase(disabledOverride)
- else
- null
- }
+ val overrideFilePath: String
+ get() = overrideFile.absolutePath
+
+ var override: CustomQuickPhrase? = null
private set
override val isEnabled: Boolean
@@ -35,6 +30,7 @@ class BuiltinQuickPhrase(
if (override != null)
return
file.copyTo(overrideFile, overwrite = true)
+ // Update override
override = CustomQuickPhrase(overrideFile)
}
@@ -66,6 +62,21 @@ class BuiltinQuickPhrase(
override = null
}
+ /**
+ * Make sure [override] is set correctly.
+ */
+ fun evaluateOverride() {
+ override = if (overrideFile.exists())
+ CustomQuickPhrase(overrideFile)
+ else {
+ val disabledOverride = File(overrideFile.path + ".$DISABLE")
+ if (disabledOverride.exists())
+ CustomQuickPhrase(disabledOverride)
+ else
+ null
+ }
+ }
+
override fun toString(): String {
return "BuiltinQuickPhrase(file=$file, overrideFile=$overrideFile, override=$override, isEnabled=$isEnabled)"
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhrase.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhrase.kt
index 637b7c704..97aa42631 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhrase.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhrase.kt
@@ -4,10 +4,25 @@
*/
package org.fcitx.fcitx5.android.data.quickphrase
+import android.os.Parcel
+import android.os.Parcelable
+import kotlinx.serialization.Serializable
import java.io.File
-import java.io.Serializable
-abstract class QuickPhrase : Serializable {
+@Serializable(QuickPhraseSerializer::class)
+abstract class QuickPhrase : Parcelable {
+
+ override fun describeContents(): Int = 0
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeString(file.absolutePath)
+ dest.writeByte(if (this is BuiltinQuickPhrase) 1 else 0)
+ if (this is BuiltinQuickPhrase) {
+ dest.writeString(overrideFilePath)
+ } else {
+ dest.writeString(null)
+ }
+ }
abstract val file: File
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhraseSerializer.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhraseSerializer.kt
new file mode 100644
index 000000000..5a5690e1e
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/quickphrase/QuickPhraseSerializer.kt
@@ -0,0 +1,73 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.data.quickphrase
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.decodeStructure
+import kotlinx.serialization.encoding.encodeStructure
+import java.io.File
+
+object QuickPhraseSerializer : KSerializer {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("quickphrase") {
+ element("path")
+ element("isBuiltin")
+ element("override")
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun serialize(
+ encoder: Encoder,
+ value: QuickPhrase
+ ) = encoder.encodeStructure(descriptor) {
+ encodeStringElement(descriptor, 0, value.file.absolutePath)
+ encodeBooleanElement(descriptor, 1, value is BuiltinQuickPhrase)
+ encodeNullableSerializableElement(
+ descriptor,
+ 2,
+ String.serializer(),
+ value.let { it as? BuiltinQuickPhrase }?.overrideFilePath
+ )
+
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun deserialize(decoder: Decoder): QuickPhrase =
+ decoder.decodeStructure(descriptor) {
+ var path: String? = null
+ var isBuiltin = false
+ var overridePath: String? = null
+
+ while (true) {
+ when (decodeElementIndex(descriptor)) {
+ 0 -> path = decodeStringElement(descriptor, 0)
+ 1 -> isBuiltin = decodeBooleanElement(descriptor, 1)
+ 2 -> overridePath = decodeNullableSerializableElement(
+ descriptor, 2, String.serializer()
+ )
+ else -> break
+ }
+ }
+
+ val file = File(path ?: throw IllegalStateException("Path cannot be null"))
+ if (isBuiltin) {
+ BuiltinQuickPhrase(
+ file,
+ File(
+ overridePath ?: throw IllegalStateException("Override path cannot be null")
+ )
+ )
+ } else {
+ CustomQuickPhrase(file)
+ }
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/CustomThemeSerializer.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/CustomThemeSerializer.kt
index de6507e21..ebadfeb16 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/CustomThemeSerializer.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/CustomThemeSerializer.kt
@@ -55,6 +55,13 @@ object CustomThemeSerializer : JsonTransformingSerializer(Theme.Cu
private val strategies: List =
// Add migrations here
listOf(
+ MigrationStrategy("2.1") {
+ JsonObject(it.toMutableMap().apply {
+ put("candidateTextColor", getValue("keyTextColor"))
+ put("candidateLabelColor", getValue("keyTextColor"))
+ put("candidateCommentColor", getValue("altKeyTextColor"))
+ })
+ },
MigrationStrategy("2.0") {
JsonObject(it.toMutableMap().apply {
if (get("backgroundImage") != null) {
@@ -80,7 +87,7 @@ object CustomThemeSerializer : JsonTransformingSerializer(Theme.Cu
private const val VERSION = "version"
- private const val CURRENT_VERSION = "2.0"
+ private const val CURRENT_VERSION = "2.1"
private const val FALLBACK_VERSION = "1.0"
private val knownVersions = strategies.map { it.version }
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/Theme.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/Theme.kt
index 10c7e0371..d7f2791b2 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/Theme.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/Theme.kt
@@ -12,9 +12,10 @@ import android.graphics.drawable.Drawable
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
+import org.fcitx.fcitx5.android.utils.DarkenColorFilter
import org.fcitx.fcitx5.android.utils.RectSerializer
+import org.fcitx.fcitx5.android.utils.alpha
import org.fcitx.fcitx5.android.utils.appContext
-import org.fcitx.fcitx5.android.utils.DarkenColorFilter
import java.io.File
@Serializable
@@ -30,6 +31,11 @@ sealed class Theme : Parcelable {
abstract val keyBackgroundColor: Int
abstract val keyTextColor: Int
+ // Color of candidate text
+ abstract val candidateTextColor: Int
+ abstract val candidateLabelColor: Int
+ abstract val candidateCommentColor: Int
+
abstract val altKeyBackgroundColor: Int
abstract val altKeyTextColor: Int
@@ -67,6 +73,9 @@ sealed class Theme : Parcelable {
override val keyboardColor: Int,
override val keyBackgroundColor: Int,
override val keyTextColor: Int,
+ override val candidateTextColor: Int,
+ override val candidateLabelColor: Int,
+ override val candidateCommentColor: Int,
override val altKeyBackgroundColor: Int,
override val altKeyTextColor: Int,
override val accentKeyBackgroundColor: Int,
@@ -88,6 +97,7 @@ sealed class Theme : Parcelable {
val srcFilePath: String,
val brightness: Int = 70,
val cropRect: @Serializable(RectSerializer::class) Rect?,
+ val cropRotation: Int = 0
) : Parcelable {
fun toDrawable(): Drawable? {
val cropped = File(croppedFilePath)
@@ -114,6 +124,9 @@ sealed class Theme : Parcelable {
override val keyboardColor: Int,
override val keyBackgroundColor: Int,
override val keyTextColor: Int,
+ override val candidateTextColor: Int,
+ override val candidateLabelColor: Int,
+ override val candidateCommentColor: Int,
override val altKeyBackgroundColor: Int,
override val altKeyTextColor: Int,
override val accentKeyBackgroundColor: Int,
@@ -139,6 +152,9 @@ sealed class Theme : Parcelable {
keyboardColor: Number,
keyBackgroundColor: Number,
keyTextColor: Number,
+ candidateTextColor: Number,
+ candidateLabelColor: Number,
+ candidateCommentColor: Number,
altKeyBackgroundColor: Number,
altKeyTextColor: Number,
accentKeyBackgroundColor: Number,
@@ -160,6 +176,9 @@ sealed class Theme : Parcelable {
keyboardColor.toInt(),
keyBackgroundColor.toInt(),
keyTextColor.toInt(),
+ candidateTextColor.toInt(),
+ candidateLabelColor.toInt(),
+ candidateCommentColor.toInt(),
altKeyBackgroundColor.toInt(),
altKeyTextColor.toInt(),
accentKeyBackgroundColor.toInt(),
@@ -184,6 +203,9 @@ sealed class Theme : Parcelable {
keyboardColor,
keyBackgroundColor,
keyTextColor,
+ candidateTextColor,
+ candidateLabelColor,
+ candidateCommentColor,
altKeyBackgroundColor,
altKeyTextColor,
accentKeyBackgroundColor,
@@ -205,6 +227,7 @@ sealed class Theme : Parcelable {
originBackgroundImage: String,
brightness: Int = 70,
cropBackgroundRect: Rect? = null,
+ cropBackgroundRotation: Int = 0
) = Custom(
name,
isDark,
@@ -212,13 +235,17 @@ sealed class Theme : Parcelable {
croppedBackgroundImage,
originBackgroundImage,
brightness,
- cropBackgroundRect
+ cropBackgroundRect,
+ cropBackgroundRotation
),
backgroundColor,
barColor,
keyboardColor,
keyBackgroundColor,
keyTextColor,
+ candidateTextColor,
+ candidateLabelColor,
+ candidateCommentColor,
altKeyBackgroundColor,
altKeyTextColor,
accentKeyBackgroundColor,
@@ -235,4 +262,94 @@ sealed class Theme : Parcelable {
)
}
-}
\ No newline at end of file
+ @Parcelize
+ data class Monet(
+ override val name: String,
+ override val isDark: Boolean,
+ override val backgroundColor: Int,
+ override val barColor: Int,
+ override val keyboardColor: Int,
+ override val keyBackgroundColor: Int,
+ override val keyTextColor: Int,
+ override val candidateTextColor: Int,
+ override val candidateLabelColor: Int,
+ override val candidateCommentColor: Int,
+ override val altKeyBackgroundColor: Int,
+ override val altKeyTextColor: Int,
+ override val accentKeyBackgroundColor: Int,
+ override val accentKeyTextColor: Int,
+ override val keyPressHighlightColor: Int,
+ override val keyShadowColor: Int,
+ override val popupBackgroundColor: Int,
+ override val popupTextColor: Int,
+ override val spaceBarColor: Int,
+ override val dividerColor: Int,
+ override val clipboardEntryColor: Int,
+ override val genericActiveBackgroundColor: Int,
+ override val genericActiveForegroundColor: Int
+ ) : Theme() {
+ constructor(
+ isDark: Boolean,
+ surfaceContainer: Int,
+ surfaceContainerHigh: Int,
+ surfaceBright: Int,
+ onSurface: Int,
+ primary: Int,
+ onPrimary: Int,
+ secondaryContainer: Int,
+ onSurfaceVariant: Int,
+ ) : this(
+ name = "Monet" + if (isDark) "Dark" else "Light",
+ isDark = isDark,
+ backgroundColor = surfaceContainer,
+ barColor = surfaceContainerHigh,
+ keyboardColor = surfaceContainer,
+ keyBackgroundColor = surfaceBright,
+ keyTextColor = onSurface,
+ candidateTextColor = onSurface,
+ candidateLabelColor = onSurface,
+ candidateCommentColor = onSurfaceVariant,
+ altKeyBackgroundColor = secondaryContainer,
+ altKeyTextColor = onSurfaceVariant,
+ accentKeyBackgroundColor = primary,
+ accentKeyTextColor = onPrimary,
+ keyPressHighlightColor = onSurface.alpha(if (isDark) 0.2f else 0.12f),
+ keyShadowColor = 0x000000,
+ popupBackgroundColor = surfaceContainer,
+ popupTextColor = onSurface,
+ spaceBarColor = surfaceBright,
+ dividerColor = surfaceBright,
+ clipboardEntryColor = surfaceBright,
+ genericActiveBackgroundColor = primary,
+ genericActiveForegroundColor = onPrimary
+ )
+
+ @OptIn(ExperimentalStdlibApi::class)
+ fun toCustom() = Custom(
+ name = name + "#" + this.accentKeyBackgroundColor.toHexString(), // Use primary color as identifier
+ isDark = isDark,
+ backgroundImage = null,
+ backgroundColor = backgroundColor,
+ barColor = barColor,
+ keyboardColor = keyboardColor,
+ keyBackgroundColor = keyBackgroundColor,
+ keyTextColor = keyTextColor,
+ candidateTextColor = candidateTextColor,
+ candidateLabelColor = candidateLabelColor,
+ candidateCommentColor = candidateCommentColor,
+ altKeyBackgroundColor = altKeyBackgroundColor,
+ altKeyTextColor = altKeyTextColor,
+ accentKeyBackgroundColor = accentKeyBackgroundColor,
+ accentKeyTextColor = accentKeyTextColor,
+ keyPressHighlightColor = keyPressHighlightColor,
+ keyShadowColor = keyShadowColor,
+ popupBackgroundColor = popupBackgroundColor,
+ popupTextColor = popupTextColor,
+ spaceBarColor = spaceBarColor,
+ dividerColor = dividerColor,
+ clipboardEntryColor = clipboardEntryColor,
+ genericActiveBackgroundColor = genericActiveBackgroundColor,
+ genericActiveForegroundColor = genericActiveForegroundColor
+ )
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeManager.kt
index ed96be495..93ba4d47f 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeManager.kt
@@ -11,7 +11,8 @@ import androidx.annotation.RequiresApi
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
-import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
+import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceProvider
+import org.fcitx.fcitx5.android.data.theme.ThemeManager.activeTheme
import org.fcitx.fcitx5.android.utils.WeakHashSet
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.isDarkMode
@@ -36,12 +37,14 @@ object ThemeManager {
val DefaultTheme = ThemePreset.PixelDark
+ private var monetThemes = listOf(ThemeMonet.getLight(), ThemeMonet.getDark())
+
private val customThemes: MutableList = ThemeFilesManager.listThemes()
fun getTheme(name: String) =
customThemes.find { it.name == name } ?: BuiltinThemes.find { it.name == name }
- fun getAllThemes() = customThemes + BuiltinThemes
+ fun getAllThemes() = customThemes + monetThemes + BuiltinThemes
fun refreshThemes() {
customThemes.clear()
@@ -119,7 +122,7 @@ object ThemeManager {
}
@Keep
- private val onThemePrefsChange = ManagedPreference.OnChangeListener { key, _ ->
+ private val onThemePrefsChange = ManagedPreferenceProvider.OnChangeListener { key ->
if (prefs.dayNightModePrefNames.contains(key)) {
activeTheme = evaluateActiveTheme()
} else {
@@ -130,14 +133,15 @@ object ThemeManager {
fun init(configuration: Configuration) {
isDarkMode = configuration.isDarkMode()
// fire all `OnThemeChangedListener`s on theme preferences change
- prefs.managedPreferences.values.forEach {
- it.registerOnChangeListener(onThemePrefsChange)
- }
+ prefs.registerOnChangeListener(onThemePrefsChange)
_activeTheme = evaluateActiveTheme()
}
- fun onSystemDarkModeChange(isDark: Boolean) {
- isDarkMode = isDark
+ fun onSystemPlatteChange(newConfig: Configuration) {
+ isDarkMode = newConfig.isDarkMode()
+ monetThemes = listOf(ThemeMonet.getLight(), ThemeMonet.getDark())
+ // `ManagedThemePreference` finds a theme with same name in `getAllThemes()`
+ // thus `evaluateActiveTheme()` should be called after updating `monetThemes`
activeTheme = evaluateActiveTheme()
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeMonet.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeMonet.kt
new file mode 100644
index 000000000..ef9e32433
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemeMonet.kt
@@ -0,0 +1,95 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
+ */
+package org.fcitx.fcitx5.android.data.theme
+
+import android.os.Build
+import org.fcitx.fcitx5.android.utils.appContext
+
+// Ref:
+// https://github.com/material-components/material-components-android/blob/master/docs/theming/Color.md
+// https://www.figma.com/community/file/809865700885504168/material-3-android-15
+// https://material-foundation.github.io/material-theme-builder/
+
+// FIXME: SDK < 34 can only have approximate color values, maybe we can implement our own color algorithm.
+// See: https://github.com/XayahSuSuSu/Android-DataBackup/blob/e8b087fb55519c659bebdc46c0217731fe80a0d7/source/core/ui/src/main/kotlin/com/xayah/core/ui/material3/DynamicTonalPalette.kt#L185
+
+object ThemeMonet {
+ fun getLight(): Theme.Monet =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) // Real Monet colors
+ Theme.Monet(
+ isDark = false,
+ surfaceContainer = appContext.getColor(android.R.color.system_surface_container_light),
+ surfaceContainerHigh = appContext.getColor(android.R.color.system_surface_container_highest_light),
+ surfaceBright = appContext.getColor(android.R.color.system_surface_bright_light),
+ onSurface = appContext.getColor(android.R.color.system_on_surface_light),
+ primary = appContext.getColor(android.R.color.system_primary_light),
+ onPrimary = appContext.getColor(android.R.color.system_on_primary_light),
+ secondaryContainer = appContext.getColor(android.R.color.system_secondary_container_light),
+ onSurfaceVariant = appContext.getColor(android.R.color.system_on_surface_variant_light)
+ )
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) // Approximate color values
+ Theme.Monet(
+ isDark = false,
+ surfaceContainer = appContext.getColor(android.R.color.system_neutral1_50), // neutral94
+ surfaceContainerHigh = appContext.getColor(android.R.color.system_neutral2_100), // neutral92
+ surfaceBright = appContext.getColor(android.R.color.system_neutral1_10), // neutral98
+ onSurface = appContext.getColor(android.R.color.system_neutral1_900),
+ primary = appContext.getColor(android.R.color.system_accent1_600),
+ onPrimary = appContext.getColor(android.R.color.system_accent1_0),
+ secondaryContainer = appContext.getColor(android.R.color.system_accent2_100),
+ onSurfaceVariant = appContext.getColor(android.R.color.system_accent2_700)
+ )
+ else // Static MD3 colors, based on #769CDF
+ Theme.Monet(
+ isDark = false,
+ surfaceContainer = 0xffededf4.toInt(),
+ surfaceContainerHigh = 0xffe7e8ee.toInt(),
+ surfaceBright = 0xfff9f9ff.toInt(),
+ onSurface = 0xff191c20.toInt(),
+ primary = 0xff415f91.toInt(),
+ onPrimary = 0xffffffff.toInt(),
+ secondaryContainer = 0xffdae2f9.toInt(),
+ onSurfaceVariant = 0xff44474e.toInt(),
+ )
+
+ fun getDark(): Theme.Monet =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) // Real Monet colors
+ Theme.Monet(
+ isDark = true,
+ surfaceContainer = appContext.getColor(android.R.color.system_surface_container_dark),
+ surfaceContainerHigh = appContext.getColor(android.R.color.system_surface_container_high_dark),
+ surfaceBright = appContext.getColor(android.R.color.system_surface_bright_dark),
+ onSurface = appContext.getColor(android.R.color.system_on_surface_dark),
+ primary = appContext.getColor(android.R.color.system_primary_dark),
+ onPrimary = appContext.getColor(android.R.color.system_on_primary_dark),
+ secondaryContainer = appContext.getColor(android.R.color.system_secondary_container_dark),
+ onSurfaceVariant = appContext.getColor(android.R.color.system_on_surface_variant_dark)
+ )
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) // Approximate color values
+ Theme.Monet(
+ isDark = true,
+ surfaceContainer = appContext.getColor(android.R.color.system_neutral1_900), // neutral12
+ surfaceContainerHigh = appContext.getColor(android.R.color.system_neutral2_1000), // neutral17
+ surfaceBright = appContext.getColor(android.R.color.system_neutral1_800), // neutral24
+ onSurface = appContext.getColor(android.R.color.system_neutral1_100),
+ primary = appContext.getColor(android.R.color.system_accent1_200),
+ onPrimary = appContext.getColor(android.R.color.system_accent1_800),
+ secondaryContainer = appContext.getColor(android.R.color.system_accent2_700),
+ onSurfaceVariant = appContext.getColor(android.R.color.system_accent2_200)
+ )
+ else // Static MD3 colors, based on #769CDF
+ Theme.Monet(
+ isDark = true,
+ surfaceContainer = 0xff1d2024.toInt(),
+ surfaceContainerHigh = 0xff282a2f.toInt(),
+ surfaceBright = 0xff37393e.toInt(),
+ onSurface = 0xffe2e2e9.toInt(),
+ primary = 0xffaac7ff.toInt(),
+ onPrimary = 0xff0a305f.toInt(),
+ secondaryContainer = 0xff3e4759.toInt(),
+ onSurfaceVariant = 0xffc4c6d0.toInt(),
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt
index 521b56608..a42ef8d52 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt
@@ -6,10 +6,14 @@
package org.fcitx.fcitx5.android.data.theme
import android.content.SharedPreferences
+import android.os.Build
import androidx.annotation.StringRes
+import androidx.core.content.edit
+import org.fcitx.fcitx5.android.BuildConfig
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceCategory
+import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum
class ThemePrefs(sharedPreferences: SharedPreferences) :
ManagedPreferenceCategory(R.string.theme, sharedPreferences) {
@@ -76,56 +80,43 @@ class ThemePrefs(sharedPreferences: SharedPreferences) :
val keyRadius = int(R.string.key_radius, "key_radius", 4, 0, 48, "dp")
- enum class PunctuationPosition {
- Bottom,
- TopRight;
+ val textEditingButtonRadius =
+ int(R.string.text_editing_button_radius, "text_editing_button_radius", 8, 0, 48, "dp")
- companion object : ManagedPreference.StringLikeCodec {
- override fun decode(raw: String): PunctuationPosition = valueOf(raw)
- }
+ val clipboardEntryRadius =
+ int(R.string.clipboard_entry_radius, "clipboard_entry_radius", 2, 0, 48, "dp")
+
+ enum class PunctuationPosition(override val stringRes: Int) : ManagedPreferenceEnum {
+ None(R.string.punctuation_pos_none),
+ Bottom(R.string.punctuation_pos_bottom),
+ TopRight(R.string.punctuation_pos_top_right);
}
- val punctuationPosition = list(
+ val punctuationPosition = enumList(
R.string.punctuation_position,
"punctuation_position",
- PunctuationPosition.Bottom,
- PunctuationPosition,
- listOf(
- PunctuationPosition.Bottom,
- PunctuationPosition.TopRight
- ),
- listOf(
- R.string.punctuation_pos_bottom,
- R.string.punctuation_pos_top_right
- )
+ PunctuationPosition.Bottom
)
- enum class NavbarBackground {
- None,
- ColorOnly,
- Full;
-
- companion object : ManagedPreference.StringLikeCodec {
- override fun decode(raw: String): NavbarBackground = valueOf(raw)
- }
+ enum class NavbarBackground(override val stringRes: Int) : ManagedPreferenceEnum {
+ None(R.string.navbar_bkg_none),
+ ColorOnly(R.string.navbar_bkg_color_only),
+ Full(R.string.navbar_bkg_full);
}
- val navbarBackground = list(
+ val navbarBackground = enumList(
R.string.navbar_background,
"navbar_background",
- NavbarBackground.ColorOnly,
- NavbarBackground,
- listOf(
- NavbarBackground.None,
- NavbarBackground.ColorOnly,
- NavbarBackground.Full
- ),
- listOf(
- R.string.navbar_bkg_none,
- R.string.navbar_bkg_color_only,
- R.string.navbar_bkg_full
- )
- )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) NavbarBackground.Full else NavbarBackground.ColorOnly,
+ // 35+ forces edge to edge
+ enableUiOn = { Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM }
+ ).apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ sharedPreferences.edit {
+ remove(this@apply.key)
+ }
+ }
+ }
/**
* When [followSystemDayNightTheme] is disabled, this theme is used.
@@ -140,14 +131,14 @@ class ThemePrefs(sharedPreferences: SharedPreferences) :
val followSystemDayNightTheme = switch(
R.string.follow_system_day_night_theme,
"follow_system_dark_mode",
- false,
+ true,
summary = R.string.follow_system_day_night_theme_summary
)
val lightModeTheme = themePreference(
R.string.light_mode_theme,
"light_mode_theme",
- ThemePreset.PixelLight,
+ if (BuildConfig.DEBUG) ThemePreset.MaterialLight else ThemePreset.PixelLight,
enableUiOn = {
followSystemDayNightTheme.getValue()
})
@@ -155,7 +146,7 @@ class ThemePrefs(sharedPreferences: SharedPreferences) :
val darkModeTheme = themePreference(
R.string.dark_mode_theme,
"dark_mode_theme",
- ThemePreset.PixelDark,
+ if (BuildConfig.DEBUG) ThemePreset.MaterialDark else ThemePreset.PixelDark,
enableUiOn = {
followSystemDayNightTheme.getValue()
})
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePreset.kt
index 429270372..239e22cb8 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePreset.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePreset.kt
@@ -14,6 +14,9 @@ object ThemePreset {
keyboardColor = 0xffeceff1,
keyBackgroundColor = 0xfffbfbfc,
keyTextColor = 0xff37474f,
+ candidateTextColor = 0xff37474f,
+ candidateLabelColor = 0xff37474f,
+ candidateCommentColor = 0xff7a858a,
altKeyBackgroundColor = 0xffdfe2e4,
// Google Pinyin's symbol color on alphabet key: #727d82
altKeyTextColor = 0xff7a858a,
@@ -38,6 +41,9 @@ object ThemePreset {
keyboardColor = 0xff263238,
keyBackgroundColor = 0xff404a50,
keyTextColor = 0xffd9dbdc,
+ candidateTextColor = 0xffd9dbdc,
+ candidateLabelColor = 0xffd9dbdc,
+ candidateCommentColor = 0xffadb1b3,
altKeyBackgroundColor = 0xff313c42,
// Google Pinyin's symbol color on alphabet key: #b3b7b9
altKeyTextColor = 0xffadb1b3,
@@ -62,6 +68,9 @@ object ThemePreset {
keyboardColor = 0xfffafafa,
keyBackgroundColor = 0xffffffff,
keyTextColor = 0xff212121,
+ candidateTextColor = 0xff212121,
+ candidateLabelColor = 0xff212121,
+ candidateCommentColor = 0xff6e6e6e,
altKeyBackgroundColor = 0xffe1e1e1,
// Google Pinyin's symbol color on alphabet key: #4e4e4e
altKeyTextColor = 0xff6e6e6e,
@@ -86,6 +95,9 @@ object ThemePreset {
keyboardColor = 0xff2d2d2d,
keyBackgroundColor = 0xff464646,
keyTextColor = 0xfffafafa,
+ candidateTextColor = 0xfffafafa,
+ candidateLabelColor = 0xfffafafa,
+ candidateCommentColor = 0xffacacac,
altKeyBackgroundColor = 0xff373737,
// Google Pinyin's symbol color on alphabet key: #d6d6d6
altKeyTextColor = 0xffacacac,
@@ -110,6 +122,9 @@ object ThemePreset {
keyboardColor = 0xff1565c0,
keyBackgroundColor = 0xff3f80cb,
keyTextColor = 0xffffffff,
+ candidateTextColor = 0xffffffff,
+ candidateLabelColor = 0xffffffff,
+ candidateCommentColor = 0xffa9c6e7,
altKeyBackgroundColor = 0xff2771c4,
// Google Pinyin's symbol color on alphabet key: #d6d6d6
altKeyTextColor = 0xffa9c6e7,
@@ -134,6 +149,9 @@ object ThemePreset {
keyboardColor = 0xff000000,
keyBackgroundColor = 0xff2e2e2e,
keyTextColor = 0xffffffff,
+ candidateTextColor = 0xffffffff,
+ candidateLabelColor = 0xffffffff,
+ candidateCommentColor = 0xffa1a1a1,
altKeyBackgroundColor = 0xff141414,
// Google Pinyin's symbol color on alphabet key: #d9e6f5
altKeyTextColor = 0xffa1a1a1,
@@ -158,6 +176,9 @@ object ThemePreset {
keyboardColor = 0xffECEFF4,
keyBackgroundColor = 0xffECEFF4,
keyTextColor = 0xff2E3440,
+ candidateTextColor = 0xff2E3440,
+ candidateLabelColor = 0xff2E3440,
+ candidateCommentColor = 0xff4C566A,
altKeyBackgroundColor = 0xffE5E9F0,
altKeyTextColor = 0xff434C5E,
accentKeyBackgroundColor = 0xff5E81AC,
@@ -181,6 +202,9 @@ object ThemePreset {
keyboardColor = 0xff2E3440,
keyBackgroundColor = 0xff4C566A,
keyTextColor = 0xffECEFF4,
+ candidateTextColor = 0xffECEFF4,
+ candidateLabelColor = 0xffECEFF4,
+ candidateCommentColor = 0xffD8DEE9,
altKeyBackgroundColor = 0xff3B4252,
altKeyTextColor = 0xffD8DEE9,
accentKeyBackgroundColor = 0xff88C0D0,
@@ -204,6 +228,9 @@ object ThemePreset {
keyboardColor = 0xff272822,
keyBackgroundColor = 0xff33342c,
keyTextColor = 0xffd6d6d6,
+ candidateTextColor = 0xffd6d6d6,
+ candidateLabelColor = 0xffd6d6d6,
+ candidateCommentColor = 0xff797979,
altKeyBackgroundColor = 0xff2d2e27,
altKeyTextColor = 0xff797979,
accentKeyBackgroundColor = 0xffb05279,
@@ -230,6 +257,9 @@ object ThemePreset {
keyboardColor = 0x00000000,
keyBackgroundColor = 0x4bffffff,
keyTextColor = 0xffffffff,
+ candidateTextColor = 0xffffffff,
+ candidateLabelColor = 0xffffffff,
+ candidateCommentColor = 0xc9ffffff,
altKeyBackgroundColor = 0x0cffffff,
altKeyTextColor = 0xc9ffffff,
accentKeyBackgroundColor = 0xff5e97f6,
@@ -256,6 +286,9 @@ object ThemePreset {
keyboardColor = 0x00000000,
keyBackgroundColor = 0x4bffffff,
keyTextColor = 0xff000000,
+ candidateTextColor = 0xff000000,
+ candidateLabelColor = 0xff000000,
+ candidateCommentColor = 0xb9000000,
altKeyBackgroundColor = 0x0cffffff,
altKeyTextColor = 0xb9000000,
accentKeyBackgroundColor = 0xff5e97f6,
@@ -271,4 +304,4 @@ object ThemePreset {
genericActiveForegroundColor = 0xffffffff
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt
new file mode 100644
index 000000000..c43731d39
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt
@@ -0,0 +1,80 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.input
+
+import android.view.WindowInsets
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.fcitx.fcitx5.android.core.FcitxEvent
+import org.fcitx.fcitx5.android.daemon.FcitxConnection
+import org.fcitx.fcitx5.android.data.theme.Theme
+import org.fcitx.fcitx5.android.data.theme.ThemeManager
+import org.fcitx.fcitx5.android.data.theme.ThemePrefs
+import org.fcitx.fcitx5.android.utils.navbarFrameHeight
+import kotlin.math.max
+
+abstract class BaseInputView(
+ val service: FcitxInputMethodService,
+ val fcitx: FcitxConnection,
+ val theme: Theme
+) : ConstraintLayout(service) {
+
+ protected abstract fun handleFcitxEvent(it: FcitxEvent<*>)
+
+ private var eventHandlerJob: Job? = null
+
+ private fun setupFcitxEventHandler() {
+ eventHandlerJob = service.lifecycleScope.launch {
+ fcitx.runImmediately { eventFlow }.collect {
+ handleFcitxEvent(it)
+ }
+ }
+ }
+
+ var handleEvents = false
+ set(value) {
+ field = value
+ if (field) {
+ if (eventHandlerJob == null) {
+ setupFcitxEventHandler()
+ }
+ } else {
+ eventHandlerJob?.cancel()
+ eventHandlerJob = null
+ }
+ }
+
+ private val navbarBackground by ThemeManager.prefs.navbarBackground
+
+ protected fun getNavBarBottomInset(windowInsets: WindowInsets): Int {
+ if (navbarBackground != ThemePrefs.NavbarBackground.Full) {
+ return 0
+ }
+ val insets = WindowInsetsCompat.toWindowInsetsCompat(windowInsets)
+ // use navigation bar insets when available
+ val navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
+ // in case navigation bar insets goes wrong (eg. on LineageOS 21+ with gesture navigation)
+ // use mandatory system gesture insets
+ val mandatory = insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures())
+ var insetsBottom = max(navBars.bottom, mandatory.bottom)
+ if (insetsBottom <= 0) {
+ // check system gesture insets and fallback to navigation_bar_frame_height just in case
+ val gesturesBottom = insets.getInsets(WindowInsetsCompat.Type.systemGestures()).bottom
+ if (gesturesBottom > 0) {
+ insetsBottom = max(gesturesBottom, context.navbarFrameHeight())
+ }
+ }
+ return insetsBottom
+ }
+
+ override fun onDetachedFromWindow() {
+ handleEvents = false
+ super.onDetachedFromWindow()
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/CandidatesView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/CandidatesView.kt
new file mode 100644
index 000000000..c03eb61ff
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/CandidatesView.kt
@@ -0,0 +1,238 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2024-2025 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.input
+
+import android.annotation.SuppressLint
+import android.graphics.drawable.GradientDrawable
+import android.os.Build
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.WindowInsets
+import android.widget.TextView
+import androidx.annotation.Size
+import org.fcitx.fcitx5.android.R
+import org.fcitx.fcitx5.android.core.FcitxEvent
+import org.fcitx.fcitx5.android.daemon.FcitxConnection
+import org.fcitx.fcitx5.android.daemon.launchOnReady
+import org.fcitx.fcitx5.android.data.prefs.AppPrefs
+import org.fcitx.fcitx5.android.data.theme.Theme
+import org.fcitx.fcitx5.android.input.candidates.floating.PagedCandidatesUi
+import org.fcitx.fcitx5.android.input.preedit.PreeditUi
+import splitties.dimensions.dp
+import splitties.views.dsl.constraintlayout.below
+import splitties.views.dsl.constraintlayout.bottomOfParent
+import splitties.views.dsl.constraintlayout.centerHorizontally
+import splitties.views.dsl.constraintlayout.lParams
+import splitties.views.dsl.constraintlayout.matchConstraints
+import splitties.views.dsl.constraintlayout.startOfParent
+import splitties.views.dsl.constraintlayout.topOfParent
+import splitties.views.dsl.core.add
+import splitties.views.dsl.core.withTheme
+import splitties.views.dsl.core.wrapContent
+import splitties.views.padding
+import kotlin.math.roundToInt
+
+@SuppressLint("ViewConstructor")
+class CandidatesView(
+ service: FcitxInputMethodService,
+ fcitx: FcitxConnection,
+ theme: Theme
+) : BaseInputView(service, fcitx, theme) {
+
+ private val ctx = context.withTheme(R.style.Theme_InputViewTheme)
+
+ private val candidatesPrefs = AppPrefs.getInstance().candidates
+ private val orientation by candidatesPrefs.orientation
+ private val windowMinWidth by candidatesPrefs.windowMinWidth
+ private val windowPadding by candidatesPrefs.windowPadding
+ private val windowRadius by candidatesPrefs.windowRadius
+ private val fontSize by candidatesPrefs.fontSize
+ private val itemPaddingVertical by candidatesPrefs.itemPaddingVertical
+ private val itemPaddingHorizontal by candidatesPrefs.itemPaddingHorizontal
+
+ private var inputPanel = FcitxEvent.InputPanelEvent.Data()
+ private var paged = FcitxEvent.PagedCandidateEvent.Data.Empty
+
+ /**
+ * horizontal, bottom, top
+ */
+ private val anchorPosition = floatArrayOf(0f, 0f, 0f)
+ private val parentSize = floatArrayOf(0f, 0f)
+
+ private var shouldUpdatePosition = false
+
+ /**
+ * layout update may or may not cause [CandidatesView]'s size [onSizeChanged],
+ * in either case, we should reposition it
+ */
+ private val layoutListener = OnGlobalLayoutListener {
+ shouldUpdatePosition = true
+ }
+
+ /**
+ * [CandidatesView]'s position is calculated based on it's size,
+ * so we need to recalculate the position after layout,
+ * and before any actual drawing to avoid flicker
+ */
+ private val preDrawListener = OnPreDrawListener {
+ if (shouldUpdatePosition) {
+ updatePosition()
+ }
+ true
+ }
+
+ private val touchEventReceiverWindow = TouchEventReceiverWindow(this)
+
+ private val setupTextView: TextView.() -> Unit = {
+ textSize = fontSize.toFloat()
+ val v = dp(itemPaddingVertical)
+ val h = dp(itemPaddingHorizontal)
+ setPadding(h, v, h, v)
+ }
+
+ private val preeditUi = PreeditUi(ctx, theme, setupTextView)
+
+ private val candidatesUi = PagedCandidatesUi(
+ ctx, theme, setupTextView,
+ onCandidateClick = { index -> fcitx.launchOnReady { it.select(index) } },
+ onPrevPage = { fcitx.launchOnReady { it.offsetCandidatePage(-1) } },
+ onNextPage = { fcitx.launchOnReady { it.offsetCandidatePage(1) } }
+ )
+
+ private var bottomInsets = 0
+
+ override fun handleFcitxEvent(it: FcitxEvent<*>) {
+ when (it) {
+ is FcitxEvent.InputPanelEvent -> {
+ inputPanel = it.data
+ updateUi()
+ }
+ is FcitxEvent.PagedCandidateEvent -> {
+ paged = it.data
+ updateUi()
+ }
+ else -> {}
+ }
+ }
+
+ private fun evaluateVisibility(): Boolean {
+ return inputPanel.preedit.isNotEmpty() ||
+ paged.candidates.isNotEmpty() ||
+ inputPanel.auxUp.isNotEmpty() ||
+ inputPanel.auxDown.isNotEmpty()
+ }
+
+ private fun updateUi() {
+ preeditUi.update(inputPanel)
+ preeditUi.root.visibility = if (preeditUi.visible) VISIBLE else GONE
+ candidatesUi.update(paged, orientation)
+ if (evaluateVisibility()) {
+ visibility = VISIBLE
+ } else {
+ // RecyclerView won't update its items when ancestor view is GONE
+ visibility = INVISIBLE
+ touchEventReceiverWindow.dismiss()
+ }
+ }
+
+ private fun updatePosition() {
+ if (visibility != VISIBLE) {
+ // skip unnecessary updates
+ return
+ }
+ val (parentWidth, parentHeight) = parentSize
+ if (parentWidth <= 0 || parentHeight <= 0) {
+ // panic, bail
+ translationX = 0f
+ translationY = 0f
+ return
+ }
+ val (horizontal, bottom, top) = anchorPosition
+ val w: Int = width
+ val h: Int = height
+ val selfWidth = w.toFloat()
+ val selfHeight = h.toFloat()
+ val tX: Float = if (layoutDirection == LAYOUT_DIRECTION_RTL) {
+ val rtlOffset = parentWidth - horizontal
+ if (rtlOffset + selfWidth > parentWidth) selfWidth - parentWidth else -rtlOffset
+ } else {
+ if (horizontal + selfWidth > parentWidth) parentWidth - selfWidth else horizontal
+ }
+ val bottomLimit = parentHeight - bottomInsets
+ val bottomSpace = bottomLimit - bottom
+ // move CandidatesView above cursor anchor, only when
+ val tY: Float = if (
+ bottom + selfHeight > bottomLimit // bottom space is not enough
+ && top > bottomSpace // top space is larger than bottom
+ ) top - selfHeight else bottom
+ translationX = tX
+ translationY = tY
+ // update touchEventReceiverWindow's position after CandidatesView's
+ touchEventReceiverWindow.showAt(tX.roundToInt(), tY.roundToInt(), w, h)
+ shouldUpdatePosition = false
+ }
+
+ fun updateCursorAnchor(@Size(4) anchor: FloatArray, @Size(2) parent: FloatArray) {
+ val (horizontal, bottom, _, top) = anchor
+ val (parentWidth, parentHeight) = parent
+ anchorPosition[0] = horizontal
+ anchorPosition[1] = bottom
+ anchorPosition[2] = top
+ parentSize[0] = parentWidth
+ parentSize[1] = parentHeight
+ updatePosition()
+ }
+
+ init {
+ // invisible by default
+ visibility = INVISIBLE
+
+ minWidth = dp(windowMinWidth)
+ padding = dp(windowPadding)
+ background = GradientDrawable().apply {
+ setColor(theme.backgroundColor)
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = dp(windowRadius).toFloat()
+ }
+ clipToOutline = true
+ outlineProvider = ViewOutlineProvider.BACKGROUND
+ add(preeditUi.root, lParams(wrapContent, wrapContent) {
+ topOfParent()
+ startOfParent()
+ })
+ add(candidatesUi.root, lParams(matchConstraints, wrapContent) {
+ matchConstraintMinWidth = wrapContent
+ below(preeditUi.root)
+ centerHorizontally()
+ bottomOfParent()
+ })
+
+ isFocusable = false
+ layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent)
+ }
+
+ override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ bottomInsets = getNavBarBottomInset(insets)
+ }
+ return insets
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ candidatesUi.root.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
+ viewTreeObserver.addOnPreDrawListener(preDrawListener)
+ }
+
+ override fun onDetachedFromWindow() {
+ viewTreeObserver.removeOnPreDrawListener(preDrawListener)
+ candidatesUi.root.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
+ touchEventReceiverWindow.dismiss()
+ super.onDetachedFromWindow()
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt
index 78acd8624..b417f5a64 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt
@@ -1,10 +1,12 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
+
package org.fcitx.fcitx5.android.input
import android.annotation.SuppressLint
+import android.app.Dialog
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
@@ -20,6 +22,7 @@ import android.view.KeyEvent
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.InlineSuggestionsRequest
@@ -36,7 +39,6 @@ import androidx.autofill.inline.common.ViewStyle
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
@@ -46,22 +48,28 @@ import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.CapabilityFlags
import org.fcitx.fcitx5.android.core.FcitxAPI
import org.fcitx.fcitx5.android.core.FcitxEvent
+import org.fcitx.fcitx5.android.core.FcitxKeyMapping
import org.fcitx.fcitx5.android.core.FormattedText
import org.fcitx.fcitx5.android.core.KeyStates
import org.fcitx.fcitx5.android.core.KeySym
+import org.fcitx.fcitx5.android.core.ScancodeMapping
import org.fcitx.fcitx5.android.core.SubtypeManager
import org.fcitx.fcitx5.android.daemon.FcitxConnection
import org.fcitx.fcitx5.android.daemon.FcitxDaemon
import org.fcitx.fcitx5.android.data.InputFeedbacks
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
+import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceProvider
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.data.theme.ThemeManager
import org.fcitx.fcitx5.android.input.cursor.CursorRange
import org.fcitx.fcitx5.android.input.cursor.CursorTracker
import org.fcitx.fcitx5.android.utils.InputMethodUtil
import org.fcitx.fcitx5.android.utils.alpha
+import org.fcitx.fcitx5.android.utils.forceShowSelf
import org.fcitx.fcitx5.android.utils.inputMethodManager
+import org.fcitx.fcitx5.android.utils.monitorCursorAnchor
+import org.fcitx.fcitx5.android.utils.styledFloat
import org.fcitx.fcitx5.android.utils.withBatchEdit
import splitties.bitflags.hasFlag
import splitties.dimensions.dp
@@ -69,7 +77,6 @@ import splitties.resources.styledColor
import timber.log.Timber
import kotlin.math.max
-
class FcitxInputMethodService : LifecycleInputMethodService() {
private lateinit var fcitx: FcitxConnection
@@ -81,7 +88,16 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
private lateinit var pkgNameCache: PackageNameCache
+ private lateinit var decorView: View
+ private lateinit var contentView: FrameLayout
private var inputView: InputView? = null
+ private var candidatesView: CandidatesView? = null
+
+ private val navbarMgr = NavigationBarManager()
+ private val inputDeviceMgr = InputDeviceManager onChange@{
+ val w = window.window ?: return@onChange
+ navbarMgr.evaluate(w, useVirtualKeyboard = it)
+ }
private var capabilityFlags = CapabilityFlags.DefaultFlags
@@ -102,33 +118,56 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
private var highlightColor: Int = 0x66008577 // material_deep_teal_500 with alpha 0.4
- private val ignoreSystemCursor by AppPrefs.getInstance().advanced.ignoreSystemCursor
+ private val prefs = AppPrefs.getInstance()
+ private val inlineSuggestions by prefs.keyboard.inlineSuggestions
+ private val ignoreSystemCursor by prefs.advanced.ignoreSystemCursor
- private val inlineSuggestions by AppPrefs.getInstance().keyboard.inlineSuggestions
+ private val recreateInputViewPrefs: Array> = arrayOf(
+ prefs.keyboard.expandKeypressArea,
+ prefs.advanced.disableAnimation,
+ prefs.advanced.ignoreSystemWindowInsets,
+ )
- @Keep
- private val recreateInputViewListener = ManagedPreference.OnChangeListener { _, _ ->
- recreateInputView(ThemeManager.activeTheme)
+ private fun replaceInputView(theme: Theme): InputView {
+ val newInputView = InputView(this, fcitx, theme)
+ setInputView(newInputView)
+ inputDeviceMgr.setInputView(newInputView)
+ navbarMgr.setupInputView(newInputView)
+ inputView = newInputView
+ return newInputView
+ }
+
+ private fun replaceCandidateView(theme: Theme): CandidatesView {
+ val newCandidatesView = CandidatesView(this, fcitx, theme)
+ // replace CandidatesView manually
+ contentView.removeView(candidatesView)
+ // put CandidatesView directly under content view
+ contentView.addView(newCandidatesView)
+ inputDeviceMgr.setCandidatesView(newCandidatesView)
+ navbarMgr.setupInputView(newCandidatesView)
+ candidatesView = newCandidatesView
+ return newCandidatesView
+ }
+
+ private fun replaceInputViews(theme: Theme) {
+ navbarMgr.evaluate(window.window!!)
+ replaceInputView(theme)
+ replaceCandidateView(theme)
}
@Keep
- private val onThemeChangeListener = ThemeManager.OnThemeChangeListener {
- recreateInputView(it)
+ private val recreateInputViewListener = ManagedPreference.OnChangeListener { _, _ ->
+ replaceInputView(ThemeManager.activeTheme)
}
- private fun recreateInputView(theme: Theme) {
- // InputView should be created first in onCreateInputView
- // setInputView should be used to 'replace' current InputView only
- InputView(this, fcitx, theme).also {
- inputView = it
- setInputView(it)
- }
+ @Keep
+ private val recreateCandidatesViewListener = ManagedPreferenceProvider.OnChangeListener {
+ replaceCandidateView(ThemeManager.activeTheme)
}
- private fun postJob(scope: CoroutineScope, block: suspend () -> Unit): Job {
- val job = scope.launch(start = CoroutineStart.LAZY) { block() }
- jobs.trySend(job)
- return job
+ @Keep
+ private val onThemeChangeListener = ThemeManager.OnThemeChangeListener {
+ replaceInputViews(it)
}
/**
@@ -138,8 +177,13 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
* subsequent operations can start if the prior operation is not finished (suspended),
* [postFcitxJob] ensures that operations are executed sequentially.
*/
- fun postFcitxJob(block: suspend FcitxAPI.() -> Unit) =
- postJob(fcitx.lifecycleScope) { fcitx.runOnReady(block) }
+ fun postFcitxJob(block: suspend FcitxAPI.() -> Unit): Job {
+ val job = fcitx.lifecycleScope.launch(start = CoroutineStart.LAZY) {
+ fcitx.runOnReady(block)
+ }
+ jobs.trySend(job)
+ return job
+ }
override fun onCreate() {
fcitx = FcitxDaemon.connect(javaClass.name)
@@ -152,10 +196,10 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
}
pkgNameCache = PackageNameCache(this)
- AppPrefs.getInstance().apply {
- keyboard.expandKeypressArea.registerOnChangeListener(recreateInputViewListener)
- advanced.disableAnimation.registerOnChangeListener(recreateInputViewListener)
+ recreateInputViewPrefs.forEach {
+ it.registerOnChangeListener(recreateInputViewListener)
}
+ prefs.candidates.registerOnChangeListener(recreateCandidatesViewListener)
ThemeManager.addOnChangedListener(onThemeChangeListener)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
postFcitxJob {
@@ -163,6 +207,8 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
}
super.onCreate()
+ decorView = window.window!!.decorView
+ contentView = decorView.findViewById(android.R.id.content)
}
private fun handleFcitxEvent(event: FcitxEvent<*>) {
@@ -173,10 +219,16 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
is FcitxEvent.KeyEvent -> event.data.let event@{
if (it.states.virtual) {
// KeyEvent from virtual keyboard
- when (it.unicode) {
- '\b'.code -> handleBackspaceKey()
- '\r'.code -> handleReturnKey()
- else -> commitText(Char(it.unicode).toString())
+ when (it.sym.sym) {
+ FcitxKeyMapping.FcitxKey_BackSpace -> handleBackspaceKey()
+ FcitxKeyMapping.FcitxKey_Return -> handleReturnKey()
+ FcitxKeyMapping.FcitxKey_Left -> sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT)
+ FcitxKeyMapping.FcitxKey_Right -> sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT)
+ else -> if (it.unicode > 0) {
+ commitText(Character.toString(it.unicode))
+ } else {
+ Timber.w("Unhandled Virtual KeyEvent: $it")
+ }
}
} else {
// KeyEvent from physical keyboard (or input method engine forwardKey)
@@ -198,7 +250,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
} else {
// no matching keyCode, commit character once on key down
if (!it.up && it.unicode > 0) {
- commitText(Char(it.unicode).toString())
+ commitText(Character.toString(it.unicode))
} else {
Timber.w("Unhandled Fcitx KeyEvent: $it")
}
@@ -333,7 +385,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
0,
metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD,
- 0,
+ ScancodeMapping.keyCodeToScancode(keyEventCode),
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
)
)
@@ -349,7 +401,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
0,
metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD,
- 0,
+ ScancodeMapping.keyCodeToScancode(keyEventCode),
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
)
)
@@ -409,31 +461,35 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
override fun onWindowShown() {
super.onWindowShown()
+ try {
+ highlightColor = styledColor(android.R.attr.colorAccent).alpha(0.4f)
+ } catch (_: Exception) {
+ Timber.w("Device does not support android.R.attr.colorAccent which it should have.")
+ }
InputFeedbacks.syncSystemPrefs()
+ // navbar foreground/background color would reset every time window shows
+ navbarMgr.update(window.window!!)
}
- override fun onCreateInputView(): View {
- // onCreateInputView will be called once, when the input area is first displayed,
- // during each onConfigurationChanged period.
- // That is, onCreateInputView would be called again, after system dark mode changes,
- // or screen orientation changes.
- return InputView(this, fcitx, ThemeManager.activeTheme).also {
- inputView = it
- }
+ override fun onCreateInputView(): View? {
+ replaceInputViews(ThemeManager.activeTheme)
+ // We will call `setInputView` by ourselves. This is fine.
+ return null
}
override fun setInputView(view: View) {
- try {
- highlightColor = view.styledColor(android.R.attr.colorAccent).alpha(0.4f)
- } catch (e: Exception) {
- Timber.w("Device does not support android.R.attr.colorAccent which it should have.")
- }
- window.window!!.decorView
- .findViewById(android.R.id.inputArea)
+ super.setInputView(view)
+ // input method layout has not changed in 11 years:
+ // https://android.googlesource.com/platform/frameworks/base/+/ae3349e1c34f7aceddc526cd11d9ac44951e97b6/core/res/res/layout/input_method.xml
+ // expand inputArea to fullscreen
+ contentView.findViewById(android.R.id.inputArea)
.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
- super.setInputView(view)
+ /**
+ * expand InputView to fullscreen, since [android.inputmethodservice.InputMethodService.setInputView]
+ * would set InputView's height to [ViewGroup.LayoutParams.WRAP_CONTENT]
+ */
view.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
@@ -443,21 +499,33 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
win.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
+ private var inputViewLocation = intArrayOf(0, 0)
+
override fun onComputeInsets(outInsets: Insets) {
- val (_, y) = intArrayOf(0, 0).also { inputView?.keyboardView?.getLocationInWindow(it) }
- outInsets.apply {
- contentTopInsets = y
- touchableInsets = Insets.TOUCHABLE_INSETS_CONTENT
- touchableRegion.setEmpty()
- visibleTopInsets = y
+ if (inputDeviceMgr.isVirtualKeyboard) {
+ inputView?.keyboardView?.getLocationInWindow(inputViewLocation)
+ outInsets.apply {
+ contentTopInsets = inputViewLocation[1]
+ visibleTopInsets = inputViewLocation[1]
+ touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
+ }
+ } else {
+ val n = decorView.findViewById(android.R.id.navigationBarBackground)?.height ?: 0
+ val h = decorView.height - n
+ outInsets.apply {
+ contentTopInsets = h
+ visibleTopInsets = h
+ touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
+ }
}
}
- // TODO: candidate view for physical keyboard input
- // always show InputView since we do not support physical keyboard input without it yet
+ // always show InputView since we delegate CandidatesView's visibility to it
@SuppressLint("MissingSuperCall")
override fun onEvaluateInputViewShown() = true
+ fun superEvaluateInputViewShown() = super.onEvaluateInputViewShown()
+
override fun onEvaluateFullscreenMode() = false
private fun forwardKeyEvent(event: KeyEvent): Boolean {
@@ -471,16 +539,21 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
// try send charCode first, allow upper case and lower case character generating different KeySym
// skip \t, because it's charCode is different from KeySym
// skip \n, because fcitx wants \r for return
- if (charCode > 0 && charCode != '\t'.code && charCode != '\n'.code) {
+ // skip ' ', because it would produce same KeySym regardless of the modifier
+ if (charCode > 0 && charCode != '\t'.code && charCode != '\n'.code && charCode != ' '.code) {
+ // drop modifier state when using combination keys to input number/symbol on some phones
+ // because fcitx doesn't recognize selection key with modifiers (eg. Alt+Q for 1)
+ // in which case event.getNumber().toInt() == event.getUnicodeChar()
+ val s = if (event.number.code == charCode) KeyStates.Empty else states
postFcitxJob {
- sendKey(charCode, states.states, up, timestamp)
+ sendKey(charCode, s.states, event.scanCode, up, timestamp)
}
return true
}
val keySym = KeySym.fromKeyEvent(event)
if (keySym != null) {
postFcitxJob {
- sendKey(keySym, states, up, timestamp)
+ sendKey(keySym, states, event.scanCode, up, timestamp)
}
return true
}
@@ -489,6 +562,13 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ // request to show floating CandidatesView when pressing physical keyboard
+ if (inputDeviceMgr.evaluateOnKeyDown(event, this)) {
+ postFcitxJob {
+ focus(true)
+ }
+ forceShowSelf()
+ }
return forwardKeyEvent(event) || super.onKeyDown(keyCode, event)
}
@@ -496,6 +576,21 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
return forwardKeyEvent(event) || super.onKeyUp(keyCode, event)
}
+ // Added in API level 14, deprecated in 29
+ @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+ override fun onViewClicked(focusChanged: Boolean) {
+ super.onViewClicked(focusChanged)
+ if (Build.VERSION.SDK_INT < 34) {
+ inputDeviceMgr.evaluateOnViewClicked(this)
+ }
+ }
+
+ @RequiresApi(34)
+ override fun onUpdateEditorToolType(toolType: Int) {
+ super.onUpdateEditorToolType(toolType)
+ inputDeviceMgr.evaluateOnUpdateEditorToolType(toolType, this)
+ }
+
private var firstBindInput = true
override fun onBindInput() {
@@ -578,9 +673,20 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
postFcitxJob {
focus(true)
}
- // because onStartInputView will always be called after onStartInput,
- // editorInfo and capFlags should be up-to-date
- inputView?.startInput(info, capabilityFlags, restarting)
+ if (inputDeviceMgr.evaluateOnStartInputView(info, this)) {
+ // because onStartInputView will always be called after onStartInput,
+ // editorInfo and capFlags should be up-to-date
+ inputView?.startInput(info, capabilityFlags, restarting)
+ } else {
+ if (currentInputConnection?.monitorCursorAnchor() != true) {
+ if (!decorLocationUpdated) {
+ updateDecorLocation()
+ }
+ // anchor CandidatesView to bottom-left corner in case InputConnection does not
+ // support monitoring CursorAnchorInfo
+ workaroundNullCursorAnchorInfo()
+ }
+ }
}
override fun onUpdateSelection(
@@ -598,8 +704,70 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
inputView?.updateSelection(newSelStart, newSelEnd)
}
+ private val contentSize = floatArrayOf(0f, 0f)
+ private val decorLocation = floatArrayOf(0f, 0f)
+ private val decorLocationInt = intArrayOf(0, 0)
+ private var decorLocationUpdated = false
+
+ private fun updateDecorLocation() {
+ contentSize[0] = contentView.width.toFloat()
+ contentSize[1] = contentView.height.toFloat()
+ decorView.getLocationOnScreen(decorLocationInt)
+ decorLocation[0] = decorLocationInt[0].toFloat()
+ decorLocation[1] = decorLocationInt[1].toFloat()
+ // contentSize and decorLocation can be completely wrong,
+ // when measuring right after the very first onStartInputView() of an IMS' lifecycle
+ if (contentSize[0] > 0 && contentSize[1] > 0) {
+ decorLocationUpdated = true
+ }
+ }
+
+ private val anchorPosition = floatArrayOf(0f, 0f, 0f, 0f)
+
+ /**
+ * anchor candidates view to bottom-left corner, only works if [decorLocationUpdated]
+ */
+ private fun workaroundNullCursorAnchorInfo() {
+ anchorPosition[0] = 0f
+ anchorPosition[1] = contentSize[1]
+ anchorPosition[2] = 0f
+ anchorPosition[3] = contentSize[1]
+ candidatesView?.updateCursorAnchor(anchorPosition, contentSize)
+ }
+
override fun onUpdateCursorAnchorInfo(info: CursorAnchorInfo) {
- // CursorAnchorInfo focus more on screen coordinates rather than selection
+ val bounds = info.getCharacterBounds(0)
+ if (bounds != null) {
+ // anchor to start of composing span instead of insertion mark if available
+ val horizontal =
+ if (candidatesView?.layoutDirection == View.LAYOUT_DIRECTION_RTL) bounds.right else bounds.left
+ anchorPosition[0] = horizontal
+ anchorPosition[1] = bounds.bottom
+ anchorPosition[2] = horizontal
+ anchorPosition[3] = bounds.top
+ } else {
+ anchorPosition[0] = info.insertionMarkerHorizontal
+ anchorPosition[1] = info.insertionMarkerBottom
+ anchorPosition[2] = info.insertionMarkerHorizontal
+ anchorPosition[3] = info.insertionMarkerTop
+ }
+ // avoid calling `decorView.getLocationOnScreen` repeatedly
+ if (!decorLocationUpdated) {
+ updateDecorLocation()
+ }
+ if (anchorPosition.any(Float::isNaN)) {
+ // anchor candidates view to bottom-left corner in case CursorAnchorInfo is invalid
+ workaroundNullCursorAnchorInfo()
+ return
+ }
+ // params of `Matrix.mapPoints` must be [x0, y0, x1, y1]
+ info.matrix.mapPoints(anchorPosition)
+ val (xOffset, yOffset) = decorLocation
+ anchorPosition[0] -= xOffset
+ anchorPosition[1] -= yOffset
+ anchorPosition[2] -= xOffset
+ anchorPosition[3] -= yOffset
+ candidatesView?.updateCursorAnchor(anchorPosition, contentSize)
}
private fun handleCursorUpdate(newSelStart: Int, newSelEnd: Int, updateIndex: Int) {
@@ -651,7 +819,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
// because setComposingText(text, cursor) can only put cursor at end of composing,
- // sometimes onUpdateCursorAnchorInfo/onUpdateSelection would receive event with wrong cursor position.
+ // sometimes onUpdateSelection would receive event with wrong cursor position.
// those events need to be filtered.
// because of https://android.googlesource.com/platform/frameworks/base.git/+/refs/tags/android-11.0.0_r45/core/java/android/view/inputmethod/BaseInputConnection.java#851
// it's not possible to set cursor inside composing text
@@ -659,7 +827,18 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
val ic = currentInputConnection ?: return
val lastSelection = selection.latest
ic.beginBatchEdit()
- if (!composingText.spanEquals(text)) {
+ if (composingText.spanEquals(text)) {
+ // composing text content is up-to-date
+ // update cursor only when it's not empty AND cursor position is valid
+ if (text.length > 0 && text.cursor >= 0) {
+ val p = text.cursor + composing.start
+ if (p != lastSelection.start) {
+ Timber.d("updateComposingText: set Android selection ($p, $p)")
+ ic.setSelection(p, p)
+ selection.predict(p)
+ }
+ }
+ } else {
// composing text content changed
Timber.d("updateComposingText: '$text' lastSelection=$lastSelection")
if (text.isEmpty()) {
@@ -691,17 +870,6 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
}
Timber.d("updateComposingText: composing=$composing")
- } else {
- // composing text content is up-to-date
- // update cursor only when it's not empty AND cursor position is valid
- if (text.length > 0 && text.cursor >= 0) {
- val p = text.cursor + composing.start
- if (p != lastSelection.start) {
- Timber.d("updateComposingText: set Android selection ($p, $p)")
- ic.setSelection(p, p)
- selection.predict(p)
- }
- }
}
composingText = text
ic.endBatchEdit()
@@ -722,7 +890,8 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
- if (!inlineSuggestions) return null
+ // ignore inline suggestion when disabled by user || using physical keyboard with floating candidates view
+ if (!inlineSuggestions || !inputDeviceMgr.isVirtualKeyboard) return null
val theme = ThemeManager.activeTheme
val chipDrawable =
if (theme.isDark) R.drawable.bkg_inline_suggestion_dark else R.drawable.bkg_inline_suggestion_light
@@ -778,26 +947,23 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
- if (!inlineSuggestions) return false
- return inputView?.handleInlineSuggestions(response) ?: false
- }
-
- fun nextInputMethodApp() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- switchToNextInputMethod(false)
- } else {
- @Suppress("DEPRECATION")
- inputMethodManager.switchToNextInputMethod(window.window!!.attributes.token, false)
- }
+ if (!inlineSuggestions || !inputDeviceMgr.isVirtualKeyboard) return false
+ return inputView?.handleInlineSuggestions(response) == true
}
override fun onFinishInputView(finishingInput: Boolean) {
Timber.d("onFinishInputView: finishingInput=$finishingInput")
- currentInputConnection?.finishComposingText()
+ decorLocationUpdated = false
+ inputDeviceMgr.onFinishInputView()
+ currentInputConnection?.apply {
+ finishComposingText()
+ monitorCursorAnchor(false)
+ }
+ resetComposingState()
postFcitxJob {
focus(false)
}
- inputView?.finishInput()
+ showingDialog?.dismiss()
}
override fun onFinishInput() {
@@ -818,18 +984,39 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
override fun onDestroy() {
- AppPrefs.getInstance().apply {
- keyboard.expandKeypressArea.unregisterOnChangeListener(recreateInputViewListener)
- advanced.disableAnimation.unregisterOnChangeListener(recreateInputViewListener)
+ recreateInputViewPrefs.forEach {
+ it.unregisterOnChangeListener(recreateInputViewListener)
}
+ prefs.candidates.unregisterOnChangeListener(recreateCandidatesViewListener)
ThemeManager.removeOnChangedListener(onThemeChangeListener)
super.onDestroy()
// Fcitx might be used in super.onDestroy()
FcitxDaemon.disconnect(javaClass.name)
}
+ private var showingDialog: Dialog? = null
+
+ fun showDialog(dialog: Dialog) {
+ showingDialog?.dismiss()
+ dialog.window?.also {
+ it.attributes.apply {
+ token = decorView.windowToken
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG
+ }
+ it.addFlags(
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM or WindowManager.LayoutParams.FLAG_DIM_BEHIND
+ )
+ it.setDimAmount(styledFloat(android.R.attr.backgroundDimAmount))
+ }
+ dialog.setOnDismissListener {
+ showingDialog = null
+ }
+ dialog.show()
+ showingDialog = dialog
+ }
+
+ @Suppress("ConstPropertyName")
companion object {
const val DeleteSurroundingFlag = "org.fcitx.fcitx5.android.DELETE_SURROUNDING"
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt
new file mode 100644
index 000000000..656f40dfd
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt
@@ -0,0 +1,146 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.input
+
+import android.text.InputType
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import org.fcitx.fcitx5.android.data.prefs.AppPrefs
+import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesMode
+import org.fcitx.fcitx5.android.utils.monitorCursorAnchor
+
+class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
+
+ private var inputView: InputView? = null
+ private var candidatesView: CandidatesView? = null
+
+ private fun setupInputViewEvents(isVirtual: Boolean) {
+ inputView?.handleEvents = isVirtual
+ inputView?.visibility = if (isVirtual) View.VISIBLE else View.GONE
+ }
+
+ private fun setupCandidatesViewEvents(isVirtual: Boolean) {
+ candidatesView?.handleEvents = !isVirtual
+ // hide CandidatesView when entering virtual keyboard mode,
+ // but preserve the visibility when entering physical keyboard mode (in case it's empty)
+ if (isVirtual) {
+ candidatesView?.visibility = View.GONE
+ }
+ }
+
+ private fun setupViewEvents(isVirtual: Boolean) {
+ setupInputViewEvents(isVirtual)
+ setupCandidatesViewEvents(isVirtual)
+ }
+
+ var isVirtualKeyboard = true
+ private set(value) {
+ field = value
+ setupViewEvents(value)
+ }
+
+ fun setInputView(inputView: InputView) {
+ this.inputView = inputView
+ setupInputViewEvents(this.isVirtualKeyboard)
+ }
+
+ fun setCandidatesView(candidatesView: CandidatesView) {
+ this.candidatesView = candidatesView
+ setupCandidatesViewEvents(this.isVirtualKeyboard)
+ }
+
+ private fun applyMode(service: FcitxInputMethodService, useVirtualKeyboard: Boolean) {
+ if (useVirtualKeyboard == isVirtualKeyboard) {
+ return
+ }
+ // monitor CursorAnchorInfo when switching to CandidatesView
+ service.currentInputConnection.monitorCursorAnchor(!useVirtualKeyboard)
+ service.postFcitxJob {
+ setCandidatePagingMode(if (useVirtualKeyboard) 0 else 1)
+ }
+ isVirtualKeyboard = useVirtualKeyboard
+ onChange(isVirtualKeyboard)
+ }
+
+ private var startedInputView = false
+ private var isNullInputType = true
+
+ private var candidatesViewMode by AppPrefs.getInstance().candidates.mode
+
+ /**
+ * @return should use virtual keyboard
+ */
+ fun evaluateOnStartInputView(info: EditorInfo, service: FcitxInputMethodService): Boolean {
+ startedInputView = true
+ isNullInputType = info.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL
+ val useVirtualKeyboard = when (candidatesViewMode) {
+ FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
+ FloatingCandidatesMode.InputDevice -> isVirtualKeyboard
+ FloatingCandidatesMode.Disabled -> true
+ }
+ applyMode(service, useVirtualKeyboard)
+ return useVirtualKeyboard
+ }
+
+ /**
+ * @return should force show input views
+ */
+ fun evaluateOnKeyDown(e: KeyEvent, service: FcitxInputMethodService): Boolean {
+ if (startedInputView) {
+ // filter out back/home/volume buttons and combination keys
+ if (e.isPrintingKey && e.hasNoModifiers()) {
+ // evaluate virtual keyboard visibility when pressing physical keyboard while InputView visible
+ evaluateOnKeyDownInner(service)
+ }
+ // no need to force show InputView since it's already visible
+ return false
+ } else {
+ // force show InputView when focusing on text input (likely inputType is not TYPE_NULL)
+ // and pressing any digit/letter/punctuation key on physical keyboard
+ val showInputView = !isNullInputType && e.isPrintingKey && e.hasNoModifiers()
+ if (showInputView) {
+ evaluateOnKeyDownInner(service)
+ }
+ return showInputView
+ }
+ }
+
+ private fun evaluateOnKeyDownInner(service: FcitxInputMethodService) {
+ val useVirtualKeyboard = when (candidatesViewMode) {
+ FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
+ FloatingCandidatesMode.InputDevice -> false
+ FloatingCandidatesMode.Disabled -> true
+ }
+ applyMode(service, useVirtualKeyboard)
+ }
+
+ fun evaluateOnViewClicked(service: FcitxInputMethodService) {
+ if (!startedInputView) return
+ val useVirtualKeyboard = when (candidatesViewMode) {
+ FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
+ else -> true
+ }
+ applyMode(service, useVirtualKeyboard)
+ }
+
+ fun evaluateOnUpdateEditorToolType(toolType: Int, service: FcitxInputMethodService) {
+ if (!startedInputView) return
+ val useVirtualKeyboard = when (candidatesViewMode) {
+ FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
+ FloatingCandidatesMode.InputDevice ->
+ // switch to virtual keyboard on touch screen events, otherwise preserve current mode
+ if (toolType == MotionEvent.TOOL_TYPE_FINGER || toolType == MotionEvent.TOOL_TYPE_STYLUS) true else isVirtualKeyboard
+ FloatingCandidatesMode.Disabled -> true
+ }
+ applyMode(service, useVirtualKeyboard)
+ }
+
+ fun onFinishInputView() {
+ startedInputView = false
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt
index c6872b870..52680f473 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt
@@ -1,46 +1,37 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
+
package org.fcitx.fcitx5.android.input
import android.annotation.SuppressLint
-import android.app.Dialog
import android.content.res.Configuration
-import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.View.OnClickListener
-import android.view.WindowManager
+import android.view.WindowInsets
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestionsResponse
import android.widget.ImageView
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowCompat
-import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
-import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.CapabilityFlags
import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.daemon.FcitxConnection
import org.fcitx.fcitx5.android.daemon.launchOnReady
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
-import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
+import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceProvider
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.data.theme.ThemeManager
-import org.fcitx.fcitx5.android.data.theme.ThemePrefs.NavbarBackground
import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent
import org.fcitx.fcitx5.android.input.broadcast.InputBroadcaster
import org.fcitx.fcitx5.android.input.broadcast.PreeditEmptyStateComponent
import org.fcitx.fcitx5.android.input.broadcast.PunctuationComponent
import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent
-import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent
+import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateComponent
import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener
import org.fcitx.fcitx5.android.input.keyboard.KeyboardWindow
import org.fcitx.fcitx5.android.input.picker.emojiPicker
@@ -49,7 +40,6 @@ import org.fcitx.fcitx5.android.input.picker.symbolPicker
import org.fcitx.fcitx5.android.input.popup.PopupComponent
import org.fcitx.fcitx5.android.input.preedit.PreeditComponent
import org.fcitx.fcitx5.android.input.wm.InputWindowManager
-import org.fcitx.fcitx5.android.utils.styledFloat
import org.fcitx.fcitx5.android.utils.unset
import org.mechdancer.dependency.DynamicScope
import org.mechdancer.dependency.manager.wrapToUniqueComponent
@@ -77,16 +67,12 @@ import splitties.views.imageDrawable
@SuppressLint("ViewConstructor")
class InputView(
- val service: FcitxInputMethodService,
- val fcitx: FcitxConnection,
- val theme: Theme
-) : ConstraintLayout(service) {
-
- private var shouldUpdateNavbarForeground = false
- private var shouldUpdateNavbarBackground = false
+ service: FcitxInputMethodService,
+ fcitx: FcitxConnection,
+ theme: Theme
+) : BaseInputView(service, fcitx, theme) {
private val keyBorder by ThemeManager.prefs.keyBorder
- private val navbarBackground by ThemeManager.prefs.navbarBackground
private val customBackground = imageView {
scaleType = ImageView.ScaleType.CENTER_CROP
@@ -107,8 +93,6 @@ class InputView(
setOnClickListener(placeholderOnClickListener)
}
- private val eventHandlerJob: Job
-
private val scope = DynamicScope()
private val themedContext = context.withTheme(R.style.Theme_InputViewTheme)
private val broadcaster = InputBroadcaster()
@@ -193,8 +177,10 @@ class InputView(
}
@Keep
- private val onKeyboardSizeChangeListener = ManagedPreference.OnChangeListener { _, _ ->
- updateKeyboardSize()
+ private val onKeyboardSizeChangeListener = ManagedPreferenceProvider.OnChangeListener { key ->
+ if (keyboardSizePrefs.any { it.key == key }) {
+ updateKeyboardSize()
+ }
}
val keyboardView: View
@@ -203,67 +189,21 @@ class InputView(
// MUST call before any operation
setupScope()
- eventHandlerJob = service.lifecycleScope.launch {
- fcitx.runImmediately { eventFlow }.collect {
- handleFcitxEvent(it)
- }
- }
-
// restore punctuation mapping in case of InputView recreation
fcitx.launchOnReady {
punctuation.updatePunctuationMapping(it.statusAreaActionsCached)
}
- keyboardSizePrefs.forEach {
- it.registerOnChangeListener(onKeyboardSizeChangeListener)
- }
-
// make sure KeyboardWindow's view has been created before it receives any broadcast
windowManager.addEssentialWindow(keyboardWindow, createView = true)
windowManager.addEssentialWindow(symbolPicker)
windowManager.addEssentialWindow(emojiPicker)
windowManager.addEssentialWindow(emoticonPicker)
+ // show KeyboardWindow by default
+ windowManager.attachWindow(KeyboardWindow)
broadcaster.onImeUpdate(fcitx.runImmediately { inputMethodEntryCached })
- service.window.window!!.also {
- when (navbarBackground) {
- NavbarBackground.None -> {
- WindowCompat.setDecorFitsSystemWindows(it, true)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- it.isNavigationBarContrastEnforced = true
- }
- }
- NavbarBackground.ColorOnly -> {
- shouldUpdateNavbarForeground = true
- shouldUpdateNavbarBackground = true
- WindowCompat.setDecorFitsSystemWindows(it, true)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- it.isNavigationBarContrastEnforced = false
- }
- }
- NavbarBackground.Full -> {
- shouldUpdateNavbarForeground = true
- // allow draw behind navigation bar
- WindowCompat.setDecorFitsSystemWindows(it, false)
- // transparent navigation bar
- it.navigationBarColor = Color.TRANSPARENT
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- // don't apply scrim to transparent navigation bar
- it.isNavigationBarContrastEnforced = false
- }
- ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
- insets.getInsets(WindowInsetsCompat.Type.navigationBars()).let {
- bottomPaddingSpace.updateLayoutParams {
- bottomMargin = it.bottom
- }
- }
- WindowInsetsCompat.CONSUMED
- }
- }
- }
- }
-
customBackground.imageDrawable = theme.backgroundDrawable(keyBorder)
keyboardView = constraintLayout {
@@ -317,6 +257,8 @@ class InputView(
centerVertically()
centerHorizontally()
})
+
+ keyboardPrefs.registerOnChangeListener(onKeyboardSizeChangeListener)
}
private fun updateKeyboardSize() {
@@ -329,8 +271,8 @@ class InputView(
val sidePadding = keyboardSidePaddingPx
if (sidePadding == 0) {
// hide side padding space views when unnecessary
- leftPaddingSpace.visibility = View.GONE
- rightPaddingSpace.visibility = View.GONE
+ leftPaddingSpace.visibility = GONE
+ rightPaddingSpace.visibility = GONE
windowManager.view.updateLayoutParams {
startToEnd = unset
endToStart = unset
@@ -338,8 +280,8 @@ class InputView(
endOfParent()
}
} else {
- leftPaddingSpace.visibility = View.VISIBLE
- rightPaddingSpace.visibility = View.VISIBLE
+ leftPaddingSpace.visibility = VISIBLE
+ rightPaddingSpace.visibility = VISIBLE
leftPaddingSpace.updateLayoutParams {
width = sidePadding
}
@@ -357,26 +299,17 @@ class InputView(
kawaiiBar.view.setPadding(sidePadding, 0, sidePadding, 0)
}
+ override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
+ bottomPaddingSpace.updateLayoutParams {
+ bottomMargin = getNavBarBottomInset(insets)
+ }
+ return insets
+ }
+
/**
* called when [InputView] is about to show, or restart
*/
fun startInput(info: EditorInfo, capFlags: CapabilityFlags, restarting: Boolean = false) {
- if (!restarting) {
- if (shouldUpdateNavbarForeground || shouldUpdateNavbarBackground) {
- service.window.window!!.also {
- if (shouldUpdateNavbarForeground) {
- WindowCompat.getInsetsController(it, it.decorView)
- .isAppearanceLightNavigationBars = !theme.isDark
- }
- if (shouldUpdateNavbarBackground) {
- it.navigationBarColor = when (theme) {
- is Theme.Builtin -> if (keyBorder) theme.backgroundColor else theme.keyboardColor
- is Theme.Custom -> theme.backgroundColor
- }
- }
- }
- }
- }
broadcaster.onStartInput(info, capFlags)
returnKeyDrawable.updateDrawableOnEditorInfo(info)
if (focusChangeResetKeyboard || !restarting) {
@@ -384,7 +317,7 @@ class InputView(
}
}
- private fun handleFcitxEvent(it: FcitxEvent<*>) {
+ override fun handleFcitxEvent(it: FcitxEvent<*>) {
when (it) {
is FcitxEvent.CandidateListEvent -> {
broadcaster.onCandidateUpdate(it.data)
@@ -412,49 +345,14 @@ class InputView(
broadcaster.onSelectionUpdate(start, end)
}
- private var showingDialog: Dialog? = null
-
- fun showDialog(dialog: Dialog) {
- showingDialog?.dismiss()
- val windowToken = windowToken
- check(windowToken != null) { "InputView Token is null." }
- val window = dialog.window!!
- window.attributes.apply {
- token = windowToken
- type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG
- }
- window.addFlags(
- WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM or
- WindowManager.LayoutParams.FLAG_DIM_BEHIND
- )
- window.setDimAmount(themedContext.styledFloat(android.R.attr.backgroundDimAmount))
- showingDialog = dialog.apply {
- setOnDismissListener { this@InputView.showingDialog = null }
- show()
- }
- }
-
- /**
- * called when [InputView] is being hidden
- */
- fun finishInput() {
- showingDialog?.dismiss()
- }
-
@RequiresApi(Build.VERSION_CODES.R)
fun handleInlineSuggestions(response: InlineSuggestionsResponse): Boolean {
return kawaiiBar.handleInlineSuggestions(response)
}
override fun onDetachedFromWindow() {
- keyboardSizePrefs.forEach {
- it.unregisterOnChangeListener(onKeyboardSizeChangeListener)
- }
- ViewCompat.setOnApplyWindowInsetsListener(this, null)
- showingDialog?.dismiss()
- // cancel eventHandlerJob and then clear DynamicScope,
- // implies that InputView should not be attached again after detached.
- eventHandlerJob.cancel()
+ keyboardPrefs.unregisterOnChangeListener(onKeyboardSizeChangeListener)
+ // clear DynamicScope, implies that InputView should not be attached again after detached.
scope.clear()
super.onDetachedFromWindow()
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/NavigationBarManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/NavigationBarManager.kt
new file mode 100644
index 000000000..6361670be
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/NavigationBarManager.kt
@@ -0,0 +1,121 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.input
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.Window
+import androidx.annotation.ColorInt
+import androidx.core.view.WindowCompat
+import org.fcitx.fcitx5.android.data.prefs.AppPrefs
+import org.fcitx.fcitx5.android.data.theme.Theme
+import org.fcitx.fcitx5.android.data.theme.ThemeManager
+import org.fcitx.fcitx5.android.data.theme.ThemePrefs.NavbarBackground
+import org.fcitx.fcitx5.android.utils.DeviceUtil
+
+class NavigationBarManager {
+
+ private val keyBorder by ThemeManager.prefs.keyBorder
+ private val navbarBackground by ThemeManager.prefs.navbarBackground
+
+ private var shouldUpdateNavbarForeground = false
+ private var shouldUpdateNavbarBackground = false
+
+ private fun Window.useSystemNavbarBackground(enabled: Boolean) {
+ // 35+ enforces edge to edge and we must draw behind navbar
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ WindowCompat.setDecorFitsSystemWindows(this, enabled)
+ }
+ }
+
+ private fun Window.setNavbarBackgroundColor(@ColorInt color: Int) {
+ /**
+ * Why on earth does it deprecated? It says
+ * https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-15.0.0_r3/core/java/android/view/Window.java#2720
+ * "If the app targets VANILLA_ICE_CREAM or above, the color will be transparent and cannot be changed"
+ * but it only takes effect on API 35+ devices. Older devices still needs this.
+ */
+ @Suppress("DEPRECATION")
+ navigationBarColor = color
+ }
+
+ private fun Window.enforceNavbarContrast(enforced: Boolean) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ isNavigationBarContrastEnforced = enforced
+ }
+ }
+
+ fun evaluate(window: Window) {
+ when (navbarBackground) {
+ NavbarBackground.None -> {
+ shouldUpdateNavbarForeground = false
+ shouldUpdateNavbarBackground = false
+ window.useSystemNavbarBackground(true)
+ window.enforceNavbarContrast(true)
+ }
+ NavbarBackground.ColorOnly -> {
+ shouldUpdateNavbarForeground = true
+ shouldUpdateNavbarBackground = true
+ window.useSystemNavbarBackground(true)
+ window.enforceNavbarContrast(false)
+ }
+ NavbarBackground.Full -> {
+ shouldUpdateNavbarForeground = true
+ shouldUpdateNavbarBackground = false
+ window.useSystemNavbarBackground(false)
+ window.setNavbarBackgroundColor(Color.TRANSPARENT)
+ window.enforceNavbarContrast(false)
+ // it seems One UI 7.0 (Android 15) does not allow drawing behind navbar
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && DeviceUtil.isSamsungOneUI) {
+ shouldUpdateNavbarBackground = true
+ }
+ }
+ }
+ }
+
+ fun evaluate(window: Window, useVirtualKeyboard: Boolean) {
+ if (useVirtualKeyboard) {
+ evaluate(window)
+ } else {
+ shouldUpdateNavbarForeground = true
+ shouldUpdateNavbarBackground = true
+ window.useSystemNavbarBackground(true)
+ window.enforceNavbarContrast(false)
+ }
+ update(window)
+ }
+
+ fun update(window: Window) {
+ val theme = ThemeManager.activeTheme
+ if (shouldUpdateNavbarForeground) {
+ WindowCompat.getInsetsController(window, window.decorView)
+ .isAppearanceLightNavigationBars = !theme.isDark
+ }
+ if (shouldUpdateNavbarBackground) {
+ window.setNavbarBackgroundColor(
+ if (!keyBorder && theme is Theme.Builtin) theme.keyboardColor else theme.backgroundColor
+ )
+ }
+ }
+
+ private val ignoreSystemWindowInsets by AppPrefs.getInstance().advanced.ignoreSystemWindowInsets
+
+ private val emptyOnApplyWindowInsetsListener = View.OnApplyWindowInsetsListener { _, insets ->
+ insets
+ }
+
+ fun setupInputView(v: BaseInputView) {
+ if (ignoreSystemWindowInsets) {
+ // suppress the view's own onApplyWindowInsets
+ v.setOnApplyWindowInsetsListener(emptyOnApplyWindowInsetsListener)
+ } else {
+ // on API 35+, we must call requestApplyInsets() manually after replacing views,
+ // otherwise View#onApplyWindowInsets won't be called. ¯\_(ツ)_/¯
+ v.requestApplyInsets()
+ }
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/TouchEventReceiverWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/TouchEventReceiverWindow.kt
new file mode 100644
index 000000000..58aa1fe56
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/TouchEventReceiverWindow.kt
@@ -0,0 +1,57 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.input
+
+import android.annotation.SuppressLint
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.widget.PopupWindow
+
+class TouchEventReceiverWindow(
+ private val contentView: View
+) {
+ private val ctx = contentView.context
+
+ private val window = PopupWindow(object : View(ctx) {
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return contentView.dispatchTouchEvent(event)
+ }
+ }).apply {
+ // disable animation
+ animationStyle = 0
+ }
+
+ private var isWindowShowing = false
+
+ fun showAt(x: Int, y: Int, w: Int, h: Int) {
+ isWindowShowing = true
+ if (window.isShowing) {
+ window.update(x, y, w, h)
+ } else {
+ window.width = w
+ window.height = h
+ window.showAtLocation(contentView, Gravity.TOP or Gravity.START, x, y)
+ }
+ }
+
+ private val cachedLocation = intArrayOf(0, 0)
+
+ fun show() {
+ val (x, y) = cachedLocation.also { contentView.getLocationInWindow(it) }
+ val width = contentView.width
+ val height = contentView.height
+ showAt(x, y, width, height)
+ }
+
+ fun dismiss() {
+ if (isWindowShowing) {
+ isWindowShowing = false
+ window.dismiss()
+ }
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt
index 7263f3849..3319ef324 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt
@@ -40,7 +40,12 @@ object ExpandButtonStateMachine {
}
fun new(block: (State) -> Unit) =
- EventStateMachine(Hidden).apply {
+ EventStateMachine(
+ initialState = Hidden,
+ externalBooleanStates = mutableMapOf(
+ ExpandedCandidatesEmpty to true
+ )
+ ).apply {
onNewStateListener = block
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt
index 1e7dd375c..04d717186 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.input.bar
@@ -47,10 +47,10 @@ import org.fcitx.fcitx5.android.input.bar.ui.CandidateUi
import org.fcitx.fcitx5.android.input.bar.ui.IdleUi
import org.fcitx.fcitx5.android.input.bar.ui.TitleUi
import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver
-import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent
import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle
import org.fcitx.fcitx5.android.input.candidates.expanded.window.FlexboxExpandedCandidateWindow
import org.fcitx.fcitx5.android.input.candidates.expanded.window.GridExpandedCandidateWindow
+import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateComponent
import org.fcitx.fcitx5.android.input.clipboard.ClipboardWindow
import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent
import org.fcitx.fcitx5.android.input.dependency.context
@@ -58,6 +58,7 @@ import org.fcitx.fcitx5.android.input.dependency.inputMethodService
import org.fcitx.fcitx5.android.input.dependency.theme
import org.fcitx.fcitx5.android.input.editing.TextEditingWindow
import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener
+import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView
import org.fcitx.fcitx5.android.input.keyboard.KeyboardWindow
import org.fcitx.fcitx5.android.input.popup.PopupComponent
import org.fcitx.fcitx5.android.input.status.StatusAreaWindow
@@ -185,6 +186,13 @@ class KawaiiBarComponent : UniqueViewComponent