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
new file mode 100644
index 000000000..35299216a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +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.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 597c34310..8d612b32e 100644
--- a/.github/workflows/fdroid.yml
+++ b/.github/workflows/fdroid.yml
@@ -1,6 +1,17 @@
name: F-Droid
on:
+ pull_request:
+ paths:
+ - '.github/workflows/fdroid.yml'
+ - 'app/org.fcitx.fcitx5.android.yml'
+ workflow_dispatch:
+ inputs:
+ build_number:
+ description: build number on Jenkins Job fcitx5-android
+ type: string
+ required: true
+ default: lastSuccessfulBuild
repository_dispatch:
defaults:
@@ -9,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:
@@ -19,16 +30,14 @@ jobs:
- x86
- x86_64
fail-fast: false
- env:
- APP_ID: org.fcitx.fcitx5.android
steps:
- name: Fetch fdroiddata
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: f-droid/fdroiddata
- name: Fetch fdroidserver
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: f-droid/fdroidserver
path: fdroidserver
@@ -53,51 +62,62 @@ jobs:
chown -R vagrant $GITHUB_WORKSPACE
- name: Build
+ env:
+ BUILD_NUMBER: ${{ inputs.build_number || 'lastSuccessfulBuild' }}
run: |
set -x
- curl -Lo /usr/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
+ # prevent prebuilder from writing to build summary file
+ unset GITHUB_ACTIONS GITHUB_STEP_SUMMARY
+ curl -Lo /usr/bin/yq "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64"
chmod +x /usr/bin/yq
- build_metadata=$(curl https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/lastSuccessfulBuild/artifact/out/build-metadata.json)
+ build_metadata=$(curl "https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/$BUILD_NUMBER/artifact/out/build-metadata.json")
versionName=$(echo $build_metadata | yq ".versionName")
commitHash=$(echo $build_metadata | yq ".commitHash")
timestamp=$(echo $build_metadata | yq ".timestamp")
- baseVersionCode=$(curl https://raw.githubusercontent.com/fcitx5-android/fcitx5-android/$commitHash/build-logic/convention/src/main/kotlin/Versions.kt | grep "baseVersionCode =" | sed 's/.*= //')
+ baseVersionCode=$(curl -L "https://github.com/fcitx5-android/fcitx5-android/raw/$commitHash/build-logic/convention/src/main/kotlin/Versions.kt" | grep "baseVersionCode =" | sed 's/.*= //')
declare -A abi_list
abi_list=([armeabi-v7a]=1 [arm64-v8a]=2 [x86]=3 [x86_64]=4)
i=${abi_list[${{ matrix.abi }}]}
- versionCode=$(expr $baseVersionCode \* 10 + $i)
+ versionCode=$(($baseVersionCode * 10 + $i))
source /etc/profile.d/bsenv.sh
- metadata="$home_vagrant/metadata/$APP_ID.yml"
- curl -Lo $metadata "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/app/$APP_ID.yml"
+ metadata="$home_vagrant/metadata/org.fcitx.fcitx5.android.yml"
+ curl -Lo $metadata "https://github.com/${{ github.repository }}/raw/${{ github.sha }}/app/org.fcitx.fcitx5.android.yml"
+ sed -i s/%ts/$timestamp/g $metadata
sed -i s/%abi/${{ matrix.abi }}/g $metadata
yq -i ".Builds[0] |=
(.versionName = \"$versionName\") |=
(.versionCode = $versionCode) |=
(.commit = \"$commitHash\")
" $metadata
+ prebuiltTreeURL=$(curl -L \
+ -H "Accept: application/vnd.github+json" \
+ -H "Authorization: Bearer ${{ github.token }}" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "https://api.github.com/repos/${{ github.repository }}/contents/lib/fcitx5/src/main/cpp/prebuilt?ref=${{ github.sha }}" \
+ | yq .html_url)
+ prebuilderSHA=$(curl -L "${prebuiltTreeURL/\/tree\//\/raw\/}/toolchain-versions.json" | yq ".prebuilder")
+ yq -i ".Builds[0].srclibs[0] |=
+ \"fcitx5-android-prebuilder@${prebuilderSHA}\"
+ " $metadata
yq ".Builds[0]" $metadata
cp $metadata $GITHUB_WORKSPACE/metadata
- export PATH="$fdroidserver:$PATH"
- export PYTHONPATH="$fdroidserver:$fdroidserver/examples"
- export PYTHONUNBUFFERED=true
- export GRADLE_USER_HOME=$home_vagrant/.gradle
- export fdroid="sudo --preserve-env --user vagrant
- env PATH=$fdroidserver:$PATH
- env PYTHONPATH=$fdroidserver:$fdroidserver/examples
- env PYTHONUNBUFFERED=true
- env TERM=$TERM
+
+ fdroid="sudo --preserve-env --user vagrant
env HOME=$home_vagrant
- fdroid"
+ PYTHONPATH=$fdroidserver:$fdroidserver/examples
+ PYTHONUNBUFFERED=true
+ GRADLE_USER_HOME=$home_vagrant/.gradle
+ $fdroidserver/fdroid"
- build="$APP_ID:$versionCode"
+ build="org.fcitx.fcitx5.android:$versionCode"
chown -R vagrant $home_vagrant
$fdroid fetchsrclibs $build --verbose
cd $home_vagrant
$fdroid build --verbose --test --scan-binary --on-server --no-tarball $build
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: ${{ success() || failure() }}
with:
name: fdroid-${{ matrix.abi }}
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index e8c76db18..01968ac76 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -4,24 +4,24 @@ on:
pull_request:
push:
branches: [master]
+
jobs:
develop:
- strategy:
- matrix:
- os: [ubuntu-latest, macOS-latest]
- runs-on: ${{ matrix.os }}
+ runs-on: ubuntu-24.04
steps:
- name: Fetch source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- - uses: cachix/install-nix-action@v20
+ - uses: cachix/install-nix-action@v31
+ with:
+ github_access_token: ${{ secrets.GITHUB_TOKEN }}
+ - uses: cachix/cachix-action@v16
with:
- nix_path: nixpkgs=channel:nixos-unstable
- - name: Build dev shell
- run: nix develop .#noAS
- - name: Build Debug APK
+ name: fcitx5-android
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ - 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
new file mode 100644
index 000000000..43438a178
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,47 @@
+name: Publish
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - 'build-logic/**'
+ - 'lib/**'
+ - '.github/workflows/publish.yml'
+
+jobs:
+ publish:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Fetch source code
+ uses: actions/checkout@v4
+ with:
+ 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:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Setup Android environment
+ uses: android-actions/setup-android@v3
+ with:
+ packages: cmake;3.31.6
+
+ - name: Setup Gradle
+ 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 7291ccae1..dcc86c2e1 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -10,68 +10,81 @@ on:
jobs:
build_pull_request:
- runs-on: ubuntu-22.04
+ runs-on: ${{ matrix.os }}
strategy:
+ fail-fast: false
matrix:
- abi:
- - armeabi-v7a
- - arm64-v8a
- - x86
- - x86_64
- env:
- BUILD_ABI: ${{ matrix.abi }}
+ os:
+ - ubuntu-24.04
+ - macos-13
+ - macos-14
+ - windows-2022
steps:
- name: Fetch source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Setup Android environment
- uses: android-actions/setup-android@v2
+ uses: android-actions/setup-android@v3
- name: Install Android NDK
run: |
- sdkmanager --install "cmake;3.22.1"
+ sdkmanager --install "cmake;3.31.6"
- - name: Install system dependencies
+ - name: Install system dependencies (Ubuntu)
+ if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
sudo apt update
sudo apt install extra-cmake-modules gettext
+ - name: Install system dependencies (macOS)
+ if: ${{ startsWith(matrix.os, 'macos') }}
+ run: |
+ brew install extra-cmake-modules
+
+ - name: Install system dependencies (Windows)
+ 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/gradle-build-action@v2
+ 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@v3
+ uses: actions/upload-artifact@v4
with:
- name: app-${{ matrix.abi }}
- path: app/build/outputs/apk/debug/
+ name: app-${{ matrix.os }}
+ path: app/build/outputs/apk/release/
- name: Pack plugins
+ shell: bash
run: |
mkdir plugins-to-upload
for i in $(ls plugin)
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@v3
+ uses: actions/upload-artifact@v4
with:
- name: plugins-${{ matrix.abi }}
+ name: plugins-${{ matrix.os }}
path: plugins-to-upload
diff --git a/.gitignore b/.gitignore
index 9ac32bd93..0bd6d8615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,34 +1,20 @@
# 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
-# Module :plugin:anthy
+# Plugins
# Installed data
-/plugin/anthy/src/main/assets/usr/share/fcitx5/addon
-/plugin/anthy/src/main/assets/usr/share/fcitx5/anthy
-/plugin/anthy/src/main/assets/usr/share/fcitx5/inputmethod
-/plugin/anthy/src/main/assets/usr/share/locale
+/plugin/*/src/main/assets/usr/
# Generated asset descriptor
-/plugin/anthy/src/main/assets/descriptor.json
+/plugin/*/src/main/assets/descriptor.json
# Intellij
.idea/*
!.idea/codeStyles/
+!.idea/copyright/
!.idea/dictionaries/
-!.idea/modules/
-.idea/modules/*
-# tell Andriod Stuido to exclude prebuilt dir in this file
-!.idea/modules/fcitx5-android.iml
# below are generated by Android Studio
@@ -113,3 +99,6 @@ lint/tmp/
# Android Profiling
*.hprof
+
+### Kotlin ###
+.kotlin/
diff --git a/.gitmodules b/.gitmodules
index 25c9c6748..2f3d38b6d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,24 +1,58 @@
[submodule "lib/fcitx5/src/main/cpp/fcitx5"]
path = lib/fcitx5/src/main/cpp/fcitx5
- url = git@github.com:fcitx/fcitx5.git
-[submodule "app/src/main/cpp/fcitx5-chinese-addons"]
- path = app/src/main/cpp/fcitx5-chinese-addons
- url = git@github.com:fcitx/fcitx5-chinese-addons.git
-[submodule "app/src/main/cpp/libime"]
- path = app/src/main/cpp/libime
- url = git@github.com:fcitx/libime.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
-[submodule "app/src/main/cpp/fcitx5-lua"]
- path = app/src/main/cpp/fcitx5-lua
- url = git@github.com:fcitx/fcitx5-lua.git
-[submodule "app/src/main/cpp/fcitx5-unikey"]
- path = app/src/main/cpp/fcitx5-unikey
- url = git@github.com:fcitx/fcitx5-unikey.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 = https://github.com/fcitx/fcitx5-lua.git
+[submodule "lib/libime/src/main/cpp/libime"]
+ path = lib/libime/src/main/cpp/libime
+ 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 = 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:fcitx5-android/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 = https://github.com/fcitx/fcitx5-unikey.git
+[submodule "plugin/rime/src/main/cpp/fcitx5-rime"]
+ path = plugin/rime/src/main/cpp/fcitx5-rime
+ 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 = https://github.com/rime/rime-prelude.git
+[submodule "plugin/rime/src/main/cpp/rime-essay"]
+ path = plugin/rime/src/main/cpp/rime-essay
+ 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 = 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 = https://github.com/rime/rime-stroke.git
+[submodule "plugin/hangul/src/main/cpp/fcitx5-hangul"]
+ path = plugin/hangul/src/main/cpp/fcitx5-hangul
+ 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 = https://github.com/fcitx/fcitx5-chewing.git
+[submodule "plugin/sayura/src/main/cpp/fcitx5-sayura"]
+ path = plugin/sayura/src/main/cpp/fcitx5-sayura
+ 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 = https://github.com/fcitx/libime-jyutping.git
+[submodule "plugin/clipboard-filter/ClearURLsRules"]
+ path = plugin/clipboard-filter/ClearURLsRules
+ 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/copyright/fcitx5_android.xml b/.idea/copyright/fcitx5_android.xml
new file mode 100644
index 000000000..cdcc660a6
--- /dev/null
+++ b/.idea/copyright/fcitx5_android.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 000000000..08f4594e4
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 88418956f..fa009839e 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -3,18 +3,26 @@
androidfrontend
androidkeyboard
+ androidnotification
berberman
+ bopomofo
constraintlayout
cout
+ customphrase
endl
fcitx
fmtlib
icuuid
+ inputmethodservice
+ iostreams
iter
jbytes
jnicall
jobject
jstring
+ jyutping
+ kawaii
+ keypress
lgpl
libevent
libime
@@ -25,13 +33,19 @@
pkgdatadir
preedit
quickphrase
+ sayura
sdkmanager
setenv
shijienihao
shuangpin
+ sinhala
+ snackbar
spdx
stringutils
unfocus
+ unikey
+ zhuyin
+ zlib
\ No newline at end of file
diff --git a/.idea/modules/fcitx5-android.iml b/.idea/modules/fcitx5-android.iml
deleted file mode 100644
index a3390344e..000000000
--- a/.idea/modules/fcitx5-android.iml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 30e82190c..85f9bba0f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# fcitx5-android
-An attempt to run fcitx5 on Android.
+[Fcitx5](https://github.com/fcitx/fcitx5) input method framework and engines ported to Android.
## Download
@@ -12,36 +12,46 @@ Jenkins: [](https://github.com/fcitx5-android/fcitx5-android/releases)
-[
](https://f-droid.org/packages/org.fcitx.fcitx5.android)
-
-[
](https://play.google.com/store/apps/details?id=org.fcitx.fcitx5.android&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
+[
](https://f-droid.org/packages/org.fcitx.fcitx5.android)
+[
](https://play.google.com/store/apps/details?id=org.fcitx.fcitx5.android)
## Project status
-### Implemented
+### Supported Languages
+
+- English (with spell check)
+- Chinese
+ - Pinyin, Shuangpin, Wubi, Cangjie and custom tables (built-in, powered by [fcitx5-chinese-addons](https://github.com/fcitx/fcitx5-chinese-addons))
+ - Zhuyin/Bopomofo (via [Chewing Plugin](./plugin/chewing))
+ - Jyutping (via [Jyutping Plugin](./plugin/jyutping/), powered by [libime-jyutping](https://github.com/fcitx/libime-jyutping))
+- Vietnamese (via [UniKey Plugin](./plugin/unikey), supports Telex, VNI and VIQR)
+- Japanese (via [Anthy Plugin](./plugin/anthy))
+- 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
- Virtual Keyboard (layout not customizable yet)
- 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
+- Plugin System for loading addons from other installed apk
-### Work in progress
+### Planned Features
- Customizable keyboard layout
-- More input methods
+- More input methods (via plugin)
## Screenshots
|拼音, Material Light theme, key border enabled|自然码双拼, Pixel Dark theme, key border disabled|
|:-:|:-:|
-|
|
|
+|
|
|
|Emoji picker, Pixel Light theme, key border enabled|Symbol picker, Material Dark theme, key border disabled|
|:-:|:-:|
@@ -53,33 +63,54 @@ Trello kanban: https://trello.com/b/gftk6ZdV/kanban
Matrix Room: https://matrix.to/#/#fcitx5-android:mozilla.org
-Discuss on Telegram: https://t.me/+hci-DrFVWUM3NTUx ([@fcitx5_android](https://t.me/fcitx5_android) originally)
+Discuss on Telegram: [@fcitx5_android_group](https://t.me/fcitx5_android_group) ([@fcitx5_android](https://t.me/fcitx5_android) originally)
## Build
### Dependencies
-- Android SDK Platform & Build-Tools 33.
-- Android NDK (Side by side) 25 & CMake 3.22.1, they can be installed using SDK Manager in Android Studio or `sdkmanager` command line. **Note:** NDK 21 & 22 are confirmed not working with this project.
+- 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.)
### How to set up development environment
+
+Prerequisites for Windows
+
+- Enable [Developer Mode](https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development) so that symlinks can be created without administrator privilege.
+
+- Enable symlink support for `git`:
+
+ ```shell
+ git config --global core.symlinks true
+ ```
+
+
+
First, clone this repository and fetch all submodules:
-```sh
+```shell
git clone git@github.com:fcitx5-android/fcitx5-android.git
git submodule update --init --recursive
```
-Install extra-cmake-modules from your distribution software repository:
+Install `extra-cmake-modules` and `gettext` with your system package manager:
-```sh
+```shell
# For Arch Linux (Arch has gettext in it's base meta package)
sudo pacman -S extra-cmake-modules
+
# For Debian/Ubuntu
sudo apt install extra-cmake-modules gettext
+
+# For macOS
+brew install extra-cmake-modules gettext
+
+# For Windows, install MSYS2 and execute in its shell (UCRT64)
+pacman -S mingw-w64-ucrt-x86_64-extra-cmake-modules mingw-w64-ucrt-x86_64-gettext
+# then add C:\msys64\ucrt64\bin to PATH
```
Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via SDK Manager in Android Studio:
@@ -87,6 +118,9 @@ Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via
Detailed steps (screenshots)
+**Note:** These screenshots are for references and the versions in them may be out of date.
+The current recommended versions are recorded in [Versions.kt](build-logic/convention/src/main/kotlin/Versions.kt) file.
+


@@ -99,6 +133,16 @@ Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via
+### Trouble-shooting
+
+- Android Studio indexing takes forever to complete and cosumes a lot of memory.
+
+ Switch to "Project" view in the "Project" tool window (namely the file tree side bar), right click `lib/fcitx5/src/main/cpp/prebuilt` directory, then select "Mark Directory as > Excluded". You may also need to restart the IDE to interrupt ongoing indexing process.
+
+- Gradle error: "No variants found for ':app'. Check build files to ensure at least one variant exists." or "[CXX1210] /CMakeLists.txt debug|arm64-v8a : No compatible library found"
+
+ Examine if there are environment variables set such as `_JAVA_OPTIONS` or `JAVA_TOOL_OPTIONS`. You might want to clear them (maybe in the startup script `studio.sh` of Android Studio), as some gradle plugin treats anything in stderr as errors and aborts.
+
## Nix
Appropriate Android SDK with NDK is available in the development shell. The `gradlew` should work out-of-the-box, so you can install the app to your phone with `./gradlew installDebug` after applying the patch mentioned above. For development, you may want to install the unstable version of Android Studio, and point the project SDK path to `$ANDROID_SDK_ROOT` defined in the shell. Notice that Android Studio may generate wrong `local.properties` which sets the SDK location to `~/Android/SDK` (installed by SDK Manager). In such case, you need specify `sdk.dir` as the project SDK in that file manually, in case Android Studio sticks to the wrong global SDK.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d75ca7209..1f435a29b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,11 +1,9 @@
-@file:Suppress("UnstableApiUsage")
-
plugins {
- id("android-app-convention")
- id("native-app-convention")
- id("build-metadata")
- id("data-descriptor")
- id("fcitx-component")
+ id("org.fcitx.fcitx5.android.app-convention")
+ id("org.fcitx.fcitx5.android.native-app-convention")
+ id("org.fcitx.fcitx5.android.build-metadata")
+ id("org.fcitx.fcitx5.android.data-descriptor")
+ id("org.fcitx.fcitx5.android.fcitx-component")
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
@@ -18,6 +16,7 @@ android {
applicationId = "org.fcitx.fcitx5.android"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ @Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
targets(
@@ -28,18 +27,7 @@ android {
// android specific modules
"androidfrontend",
"androidkeyboard",
- // fcitx5-chinese-addons
- "pinyin",
- "scel2org5",
- "table",
- "chttrans",
- "fullwidth",
- "pinyinhelper",
- "punctuation",
- // fcitx5-lua
- "luaaddonloader",
- // fcitx5-unikey
- "unikey"
+ "androidnotification"
)
}
}
@@ -66,20 +54,31 @@ android {
buildFeatures {
viewBinding = true
}
+
+ androidResources {
+ @Suppress("UnstableApiUsage")
+ generateLocaleConfig = true
+ }
}
kotlin {
sourceSets.configureEach {
- kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/")
+ kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/$name/kotlin"))
}
}
-aboutLibraries {
- configPath = "app/licenses"
-}
-
fcitxComponent {
- installFcitx5Data = true
+ 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 {
@@ -89,6 +88,10 @@ ksp {
dependencies {
ksp(project(":codegen"))
implementation(project(":lib:fcitx5"))
+ implementation(project(":lib:fcitx5-lua"))
+ implementation(project(":lib:libime"))
+ implementation(project(":lib:fcitx5-chinese-addons"))
+ implementation(project(":lib:common"))
implementation(libs.kotlinx.coroutines)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.activity)
@@ -111,10 +114,11 @@ dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.paging)
+ implementation(libs.androidx.startup)
implementation(libs.androidx.viewpager2)
- implementation(libs.konbini)
implementation(libs.material)
- implementation(libs.arrow)
+ implementation(libs.arrow.core)
+ implementation(libs.arrow.functions)
implementation(libs.imagecropper)
implementation(libs.flexbox)
implementation(libs.dependency)
@@ -135,3 +139,13 @@ dependencies {
androidTestImplementation(libs.androidx.lifecycle.testing)
androidTestImplementation(libs.junit)
}
+
+configurations {
+ all {
+ // remove Baseline Profile Installer or whatever it is...
+ exclude(group = "androidx.profileinstaller", module = "profileinstaller")
+ // remove unwanted splitties libraries...
+ exclude(group = "com.louiscad.splitties", module = "splitties-appctx")
+ exclude(group = "com.louiscad.splitties", module = "splitties-systemservices")
+ }
+}
diff --git a/app/licenses/libraries/boost.json b/app/licenses/libraries/boost.json
index 7848e8ef2..cebc09b62 100644
--- a/app/licenses/libraries/boost.json
+++ b/app/licenses/libraries/boost.json
@@ -1,6 +1,6 @@
{
"uniqueId": "boostorg/boost",
- "artifactVersion": "1.80.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 f5799b00a..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.0.17",
+ "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 36e42980a..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.10",
+ "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 0f51e4eb9..f20f85043 100644
--- a/app/licenses/libraries/fcitx5.json
+++ b/app/licenses/libraries/fcitx5.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5",
- "artifactVersion": "5.0.23",
+ "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 ec190743e..07f2dbdc1 100644
--- a/app/licenses/libraries/fmt.json
+++ b/app/licenses/libraries/fmt.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fmtlib/fmt",
- "artifactVersion": "8.1.1",
+ "artifactVersion": "11.0.2",
"description": "Open-source formatting library for C++",
"name": "fmtlib/fmt",
"website": "https://fmt.dev",
diff --git a/app/licenses/libraries/libevent.json b/app/licenses/libraries/libevent.json
deleted file mode 100644
index b7ed73770..000000000
--- a/app/licenses/libraries/libevent.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "uniqueId": "libevent/libevent",
- "artifactVersion": "release-2.1.12-stable",
- "description": "Event notification library",
- "name": "libevent/libevent",
- "website": "https://libevent.org/",
- "tag": "native",
- "licenses": [
- "BSD-3-Clause"
- ]
-}
diff --git a/app/licenses/libraries/libime.json b/app/licenses/libraries/libime.json
index df66a50f4..18507f632 100644
--- a/app/licenses/libraries/libime.json
+++ b/app/licenses/libraries/libime.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/libime",
- "artifactVersion": "1.0.17",
+ "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
new file mode 100644
index 000000000..65d992b93
--- /dev/null
+++ b/app/licenses/libraries/libuv.json
@@ -0,0 +1,11 @@
+{
+ "uniqueId": "libuv/libuv",
+ "artifactVersion": "1.49.2",
+ "description": "Cross-platform asynchronous I/O",
+ "name": "libuv/libuv",
+ "website": "https://libuv.org/",
+ "tag": "native",
+ "licenses": [
+ "MIT"
+ ]
+}
diff --git a/app/licenses/libraries/lua.json b/app/licenses/libraries/lua.json
index e75a093c1..11c9ef4c7 100644
--- a/app/licenses/libraries/lua.json
+++ b/app/licenses/libraries/lua.json
@@ -1,6 +1,6 @@
{
"uniqueId": "lua/lua",
- "artifactVersion": "5.4.4",
+ "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 d1d58905d..ed4109ee0 100644
--- a/app/licenses/libraries/opencc.json
+++ b/app/licenses/libraries/opencc.json
@@ -1,6 +1,6 @@
{
"uniqueId": "BYVoid/OpenCC",
- "artifactVersion": "1.1.6",
+ "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 6b7957aff..7c2ffc2eb 100644
--- a/app/org.fcitx.fcitx5.android.yml
+++ b/app/org.fcitx.fcitx5.android.yml
@@ -20,11 +20,10 @@ Builds:
submodules: true
sudo:
- apt-get update
- - apt-get install -y g++ make cmake xz-utils bzip2 extra-cmake-modules gettext
- ghc libghc-shake-dev opencc libghc-js-flot-data haskell-js-dgtable-utils automake
- libtool openjdk-17-jdk-headless
- - update-java-alternatives -a
- - apt-get install -y -t bullseye-backports libime-bin fcitx5-modules
+ - apt-get install -y g++ libtool make automake gettext bzip2 xz-utils zstd pkg-config
+ 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,27 +32,24 @@ Builds:
rm:
- lib/fcitx5/src/main/cpp/prebuilt
prebuild:
- - sdkmanager 'cmake;3.22.1' 'ndk;25.0.8775105'
- - buildTimestamp=$(curl -L https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/lastSuccessfulBuild/artifact/out/build-metadata.json
- | sed -n -E 's/.*timestamp.*"(.*)"$/\1/p')
- - echo -e "\nbuildTimestamp=$buildTimestamp" >> ../gradle.properties
- - sed -i -e '/ImportQualifiedPost/d' -e 's/import \(.*\) qualified as/import
- qualified \1 as/g' $$fcitx5-android-prebuilder$$/Main.hs
+ - 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
+ - sed -i -e 's|https://maven.pkg.github.com|https://jitpack.io|g' ../lib/*/build.gradle.kts
scanignore:
- lib/fcitx5/src/main/cpp/fcitx5/src/modules/unicode/charselectdata
scandelete:
- build-logic/convention/build
build:
- pushd $$fcitx5-android-prebuilder$$
- - mkdir build
- - cd build
- - ABI=%abi ANDROID_NDK_ROOT=$$NDK$$/../25.0.8775105 CMAKE_VERSION=3.22.1 ANDROID_PLATFORM=23
- COMP_SPELL_DICT=/usr/lib/x86_64-linux-gnu/fcitx5/libexec/comp-spell-dict runghc
- ../Main.hs -j everything
+ - 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
AllowedAPKSigningKeys: e4db1e9edff13629d07de4bbf8165fe9bd8557ab55092672da8e40dbe484ecd7
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 4ef03d96c..d154724d5 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -10,6 +10,9 @@
# Keep JNI interface
-keep class org.fcitx.fcitx5.android.core.* { *; }
+-keep class org.fcitx.fcitx5.android.data.pinyin.customphrase.PinyinCustomPhrase {
+ public (...);
+}
# Keep dependency magic
-keep class ** extends org.mechdancer.dependency.Component {
@@ -17,6 +20,17 @@
boolean equals(java.lang.Object);
}
+# remove kotlin null checks
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+ static void checkNotNull(...);
+ static void checkExpressionValueIsNotNull(...);
+ static void checkNotNullExpressionValue(...);
+ static void checkReturnedValueIsNotNull(...);
+ static void checkFieldIsNotNull(...);
+ static void checkParameterIsNotNull(...);
+ static void checkNotNullParameter(...);
+}
+
# Uncomment this to preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable
diff --git a/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/4.json b/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/4.json
new file mode 100644
index 000000000..14363cb60
--- /dev/null
+++ b/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/4.json
@@ -0,0 +1,74 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "b0fe6cdac09e0d7deaff17d8b45fe565",
+ "entities": [
+ {
+ "tableName": "clipboard",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL DEFAULT -1, `type` TEXT NOT NULL DEFAULT 'text/plain', `deleted` INTEGER NOT NULL DEFAULT 0, `sensitive` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pinned",
+ "columnName": "pinned",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'text/plain'"
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "sensitive",
+ "columnName": "sensitive",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b0fe6cdac09e0d7deaff17d8b45fe565')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt b/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt
index 2defe76f7..14bb9601a 100644
--- a/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt
+++ b/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt
@@ -1,57 +1,72 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
package org.fcitx.fcitx5.android
-import android.util.Log
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.platform.app.InstrumentationRegistry
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.MainScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.runBlocking
import org.fcitx.fcitx5.android.core.Fcitx
import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.core.RawConfig
-import org.junit.*
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Assert
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import timber.log.Timber
class FcitxTest {
private companion object {
lateinit var fcitx: Fcitx
- val lifeCycleOwner = TestLifecycleOwner()
val fcitxEventChannel = Channel>(capacity = Channel.CONFLATED)
-
- fun log(str: String) = Log.d("UnitTest", str)
+ val scope = MainScope()
@BeforeClass
@JvmStatic
fun setup() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
fcitx = Fcitx(context)
- lifeCycleOwner.lifecycle.addObserver(fcitx)
- lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
// forward to our channel for point to point consuming
fcitx.eventFlow
.onEach { fcitxEventChannel.send(it) }
- .launchIn(GlobalScope)
+ .launchIn(scope)
+ fcitx.start()
// wait fcitx started
- runBlocking { receiveFirst() }
- fcitx.setEnabledIme(arrayOf("pinyin"))
- fcitx.globalConfig = RawConfig(arrayOf(
- RawConfig("Behavior", arrayOf(
- RawConfig("ShowInputMethodInformation", false)
- ))
- ))
+ runBlocking {
+ receiveFirst()
+ fcitx.setEnabledIme(arrayOf("pinyin"))
+ fcitx.setGlobalConfig(
+ RawConfig(
+ arrayOf(
+ RawConfig(
+ "Behavior", arrayOf(
+ RawConfig("ShowInputMethodInformation", false)
+ )
+ )
+ )
+ )
+ )
+ }
}
@AfterClass
@JvmStatic
fun cleanup() {
- log("cleanup")
- lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ fcitx.stop()
}
private suspend fun sendString(str: String) {
@@ -61,7 +76,7 @@ class FcitxTest {
}
}
- private suspend inline fun > receiveFirst(): T? =
+ private suspend inline fun > receiveFirst(): T? =
fcitxEventChannel.receiveAsFlow().mapNotNull { it as? T }.firstOrNull()
private suspend fun receiveFirstCandidateList() =
@@ -80,12 +95,12 @@ class FcitxTest {
private var enabledIme: List = listOf()
@Before
- fun saveEnabledIME() {
+ fun saveEnabledIME() = runBlocking {
enabledIme = fcitx.enabledIme().map { it.uniqueName }
}
@After
- fun restoreEnabledIME() {
+ fun restoreEnabledIME() = runBlocking {
fcitx.setEnabledIme(enabledIme.toTypedArray())
}
@@ -96,7 +111,7 @@ class FcitxTest {
val expected = "你好"
fcitx.select(0)
val commitString = receiveFirstCommitString()?.data
- log("commitString is $commitString")
+ Timber.i("commitString is $commitString")
Assert.assertEquals(expected, commitString)
fcitx.reset()
}
@@ -108,7 +123,7 @@ class FcitxTest {
val expected = "你好世界"
fcitx.select(0)
val commitString = receiveFirstCommitString()?.data
- log("commitString is $commitString")
+ Timber.i("commitString is $commitString")
Assert.assertEquals(expected, commitString)
fcitx.reset()
}
@@ -116,19 +131,19 @@ class FcitxTest {
@Test
fun testInputPanelStatus(): Unit = runBlocking {
fcitx.reset()
- log("after first reset: ${fcitx.isEmpty()}")
+ Timber.i("after first reset: ${fcitx.isEmpty()}")
Assert.assertEquals(true, fcitx.isEmpty())
fcitx.sendKey('a')
do {
val list = receiveFirstCandidateList()
- } while (list!!.data.isEmpty())
- log("after sending 'a': ${fcitx.isEmpty()}")
+ } while (list!!.data.candidates.isNotEmpty())
+ Timber.i("after sending 'a': ${fcitx.isEmpty()}")
Assert.assertEquals(false, fcitx.isEmpty())
fcitx.reset()
do {
val list = receiveFirstCandidateList()
- } while (list!!.data.isNotEmpty())
- log("after second reset: ${fcitx.isEmpty()}")
+ } while (list!!.data.candidates.isNotEmpty())
+ Timber.i("after second reset: ${fcitx.isEmpty()}")
Assert.assertEquals(true, fcitx.isEmpty())
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2e162315b..fcc089b60 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,10 +19,23 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -49,12 +62,9 @@
android:configChanges="orientation|screenSize"
android:exported="false"
android:label="@string/edit_theme" />
-
+ android:name=".ui.main.CropImageActivity"
+ android:exported="false" />
+
@@ -89,12 +103,17 @@
+
+
+
+
@@ -116,6 +136,16 @@
android:resource="@xml/input_method" />
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/app/src/main/assets/usr/share/fcitx5/pinyin b/app/src/main/assets/usr/share/fcitx5/pinyin
deleted file mode 120000
index 3da4da6b0..000000000
--- a/app/src/main/assets/usr/share/fcitx5/pinyin
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../lib/fcitx5/src/main/cpp/prebuilt/chinese-addons-data/pinyin
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/pinyinhelper b/app/src/main/assets/usr/share/fcitx5/pinyinhelper
deleted file mode 120000
index ff38cad3f..000000000
--- a/app/src/main/assets/usr/share/fcitx5/pinyinhelper
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../lib/fcitx5/src/main/cpp/prebuilt/chinese-addons-data/pinyinhelper
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/spell b/app/src/main/assets/usr/share/fcitx5/spell
deleted file mode 120000
index b29f79c51..000000000
--- a/app/src/main/assets/usr/share/fcitx5/spell
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../lib/fcitx5/src/main/cpp/prebuilt/spell-dict
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/table b/app/src/main/assets/usr/share/fcitx5/table
deleted file mode 120000
index cf7158e9a..000000000
--- a/app/src/main/assets/usr/share/fcitx5/table
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../lib/fcitx5/src/main/cpp/prebuilt/libime/table
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/libime b/app/src/main/assets/usr/share/libime
deleted file mode 120000
index 8b39600e1..000000000
--- a/app/src/main/assets/usr/share/libime
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../lib/fcitx5/src/main/cpp/prebuilt/libime/data
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/opencc b/app/src/main/assets/usr/share/opencc
deleted file mode 120000
index 24bf7f559..000000000
--- a/app/src/main/assets/usr/share/opencc
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../lib/fcitx5/src/main/cpp/prebuilt/opencc/data
\ No newline at end of file
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 574913a3b..9777b761f 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -1,8 +1,6 @@
cmake_minimum_required(VERSION 3.18)
-project(fcitx5-android VERSION 0.0.6)
-
-set(CMAKE_CXX_STANDARD 17)
+project(fcitx5-android VERSION ${VERSION_NAME})
# For reproducible build
add_link_options("LINKER:--hash-style=gnu,--build-id=none")
@@ -15,87 +13,78 @@ set(CMAKE_MODULE_PATH ${FCITX5_CMAKE_MODULES} ${CMAKE_MODULE_PATH})
find_package(Fcitx5Core MODULE)
find_package(Fcitx5Module MODULE)
-# install prefix for addon conf and locale
-set(CMAKE_INSTALL_PREFIX /usr)
-set(FCITX_INSTALL_PKGDATADIR /usr/share/fcitx5)
-set(FCITX_INSTALL_LOCALEDIR /usr/share/locale)
+find_package(libime REQUIRED CONFIG)
+get_target_property(LIBIME_CMAKE_MODULES libime::cmake INTERFACE_INCLUDE_DIRECTORIES)
+set(CMAKE_MODULE_PATH ${LIBIME_CMAKE_MODULES} ${CMAKE_MODULE_PATH})
+
+find_package(LibIMECore MODULE)
+find_package(LibIMEPinyin MODULE)
+find_package(LibIMETable MODULE)
+
+find_package(fcitx5-lua REQUIRED CONFIG)
+find_package(fcitx5-chinese-addons REQUIRED CONFIG)
+
+include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cmake")
add_subdirectory(po)
add_subdirectory(androidfrontend)
add_subdirectory(androidkeyboard)
+add_subdirectory(androidnotification)
-# prebuilt dir. at least it works.
-set(PREBUILT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../lib/fcitx5/src/main/cpp/prebuilt")
-
-# prebuilt fmt
-set(fmt_DIR "${PREBUILT_DIR}/fmt/${ANDROID_ABI}/lib/cmake/fmt")
-find_package(fmt)
-
-# prebuilt libevent
-set(Libevent_DIR "${PREBUILT_DIR}/libevent/${ANDROID_ABI}/lib/cmake/libevent")
-find_package(Libevent)
+# prebuilt libuv
+set(libuv_DIR "${PREBUILT_DIR}/libuv/${ANDROID_ABI}/lib/cmake/libuv")
+find_package(libuv)
# prebuilt boost
-set(BOOST_VERSION "1.80.0")
-set(BOOST_MODULES headers filesystem atomic iostreams regex)
-set(BOOST_ROOT "${PREBUILT_DIR}/boost/${ANDROID_ABI}")
-set(Boost_DIR "${BOOST_ROOT}/lib/cmake/Boost-${BOOST_VERSION}")
-foreach(mod IN LISTS BOOST_MODULES)
- set("boost_${mod}_DIR" "${BOOST_ROOT}/lib/cmake/boost_${mod}-${BOOST_VERSION}")
-endforeach()
-
-option(ENABLE_TEST "" OFF)
-set(LIBIME_INSTALL_PKGDATADIR table)
-add_subdirectory(libime)
-# kenlm/util/exception.hh uses __FILE__ macro
-target_compile_options(kenlm PRIVATE "-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}/libime/src/libime/core/kenlm=.")
+list(APPEND CMAKE_FIND_ROOT_PATH "${PREBUILT_DIR}/boost/${ANDROID_ABI}/lib/cmake")
+find_package(Boost 1.86.0 REQUIRED COMPONENTS headers iostreams CONFIG)
-# prebuilt lua
-include("${PREBUILT_DIR}/lua/${ANDROID_ABI}/lib/cmake/LuaConfig.cmake")
-
-# we are using static linking
-option(USE_DLOPEN "" OFF)
-add_subdirectory(fcitx5-lua)
-
-# prebuilt opencc
-set(OpenCC_DIR "${PREBUILT_DIR}/opencc/${ANDROID_ABI}/lib/cmake/opencc")
-find_package(OpenCC)
-
-option(ENABLE_TEST "" OFF)
-option(ENABLE_GUI "" OFF)
-option(ENABLE_BROWSER "" OFF)
-option(USE_WEBKIT "" OFF)
-option(ENABLE_CLOUDPINYIN "" OFF)
-# prefer OpenCC_DIR rather than fcitx5-chinese-addons/cmake/FindOpenCC.cmake
-set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)
-add_subdirectory(fcitx5-chinese-addons)
-# rename to include executable in apk
-set_target_properties(scel2org5 PROPERTIES OUTPUT_NAME libscel2org5.so)
-
-option(ENABLE_TEST "" OFF)
-option(ENABLE_QT "" OFF)
-add_subdirectory(fcitx5-unikey)
-# suppress "illegal character encoding in character literal" warning in unikey/data.cpp
-target_compile_options(unikey-lib PRIVATE "-Wno-invalid-source-encoding")
+set(CHINESE_ADDONS_PINYIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons/im/pinyin")
+add_library(pinyin-customphrase STATIC "${CHINESE_ADDONS_PINYIN_DIR}/customphrase.cpp")
+target_include_directories(pinyin-customphrase INTERFACE "${CHINESE_ADDONS_PINYIN_DIR}")
+target_link_libraries(pinyin-customphrase PRIVATE Fcitx5::Utils LibIME::Core)
add_library(native-lib SHARED native-lib.cpp)
target_link_libraries(native-lib
log
- libevent::core
+ libuv::uv_a
Fcitx5::Utils
Fcitx5::Config
Fcitx5::Core
Fcitx5::Module::QuickPhrase
Fcitx5::Module::Unicode
Fcitx5::Module::Clipboard
+ Boost::headers
+ Boost::iostreams
LibIME::Pinyin
- LibIME::Table)
+ LibIME::Table
+ pinyin-customphrase
+ )
+# copy module libraries from dependency lib
add_custom_target(copy-fcitx5-modules
- 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/androidfrontend/androidfrontend.cpp b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
index c49064083..620452604 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.cpp
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
@@ -1,5 +1,10 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#include
#include
+#include
#include
#include
#include
@@ -10,13 +15,13 @@
namespace fcitx {
-class AndroidInputContext : public InputContext {
+class AndroidInputContext : public InputContextV2 {
public:
AndroidInputContext(AndroidFrontend *frontend,
InputContextManager &inputContextManager,
int uid,
const std::string &pkgName)
- : InputContext(inputContextManager, pkgName),
+ : InputContextV2(inputContextManager, pkgName),
frontend_(frontend),
uid_(uid) {
created();
@@ -30,7 +35,11 @@ class AndroidInputContext : public InputContext {
[[nodiscard]] const char *frontend() const override { return "androidfrontend"; }
void commitStringImpl(const std::string &text) override {
- frontend_->commitString(text);
+ frontend_->commitString(text, -1);
+ }
+
+ void commitStringWithCursorImpl(const std::string &text, size_t cursor) override {
+ frontend_->commitString(text, static_cast(cursor));
}
void forwardKeyImpl(const ForwardKeyEvent &key) override {
@@ -38,55 +47,91 @@ class AndroidInputContext : public InputContext {
}
void deleteSurroundingTextImpl(int offset, unsigned int size) override {
- FCITX_INFO() << "DeleteSurrounding: " << offset << " " << size;
+ 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;
+ return;
+ }
+ frontend_->deleteSurrounding(before, after);
}
void updatePreeditImpl() override {
- checkClientPreeditUpdate();
+ frontend_->updateClientPreedit(filterText(inputPanel().clientPreedit()));
}
void updateInputPanel() {
- // Normally input method engine should check CapabilityFlag::Preedit before update clientPreedit,
- // and fcitx5 won't trigger UpdatePreeditEvent when that flag is not present, in which case
- // InputContext::updatePreeditImpl() won't be called.
- // However on Android, androidkeyboard uses clientPreedit unconditionally in order to provide
- // a more integrated experience, so we need to check clientPreedit update manually even if
- // clientPreedit is not enabled.
- if (!isPreeditEnabled()) {
- checkClientPreeditUpdate();
- }
- InputPanel &ip = inputPanel();
+ const InputPanel &ip = inputPanel();
frontend_->updateInputPanel(
filterText(ip.preedit()),
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) {
size = bulk->totalSize();
// limit candidate count to 16 (for paging)
- const int limit = std::min(size, 16);
+ const int limit = size < 0 ? 16 : std::min(size, 16);
for (int i = 0; i < limit; i++) {
- auto &candidate = bulk->candidateFromAll(i);
- // maybe unnecessary; I don't see anywhere using `CandidateWord::setPlaceHolder`
- // if (candidate.isPlaceHolder()) continue;
- candidates.emplace_back(filterString(candidate.text()));
+ try {
+ auto &candidate = bulk->candidateFromAll(i);
+ // maybe unnecessary; I don't see anywhere using `CandidateWord::setPlaceHolder`
+ // if (candidate.isPlaceHolder()) continue;
+ candidates.emplace_back(filterString(candidate.textWithComment()));
+ } catch (const std::invalid_argument &e) {
+ size = static_cast(candidates.size());
+ break;
+ }
}
} else {
size = list->size();
for (int i = 0; i < size; i++) {
- candidates.emplace_back(filterString(list->candidate(i).text()));
+ candidates.emplace_back(filterString(list->candidate(i).textWithComment()));
}
}
}
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;
@@ -105,42 +150,122 @@ class AndroidInputContext : public InputContext {
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();
if (list) {
+ const int last = offset + limit;
const auto &bulk = list->toBulk();
if (bulk) {
- const int _limit = std::min(bulk->totalSize(), offset + limit);
- for (int i = offset; i < _limit; i++) {
- auto &candidate = bulk->candidateFromAll(i);
- candidates.emplace_back(filterString(candidate.text()));
+ const int totalSize = bulk->totalSize();
+ const int end = totalSize < 0 ? last : std::min(totalSize, last);
+ for (int i = offset; i < end; i++) {
+ try {
+ auto &candidate = bulk->candidateFromAll(i);
+ candidates.emplace_back(filterString(candidate.textWithComment()));
+ } catch (const std::invalid_argument &e) {
+ break;
+ }
}
} else {
- const int _limit = std::min(list->size(), offset + limit);
- for (int i = offset; i < _limit; i++) {
- candidates.emplace_back(filterString(list->candidate(i).text()));
+ const int end = std::min(list->size(), last);
+ for (int i = offset; i < end; i++) {
+ candidates.emplace_back(filterString(list->candidate(i).textWithComment()));
}
}
}
return candidates;
}
-private:
- AndroidFrontend *frontend_;
- int uid_;
+ 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;
+ }
- bool clientPreeditEmpty_ = true;
+ 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 checkClientPreeditUpdate() {
- const auto &clientPreedit = filterText(inputPanel().clientPreedit());
- bool empty = clientPreedit.empty();
- // skip update if new and old clientPreedit are both empty
- if (empty && clientPreeditEmpty_) return;
- clientPreeditEmpty_ = empty;
- frontend_->updateClientPreedit(clientPreedit);
+ 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_;
+
inline Text filterText(const Text &orig) {
return frontend_->instance()->outputFilter(this, orig);
}
@@ -156,26 +281,34 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
activeIC_(nullptr),
icCache_(),
eventHandlers_(),
- statusAreaDefer_(),
- statusAreaUpdated_(false) {
+ pagingMode_(0) {
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextInputMethodActivated,
EventWatcherPhase::Default,
- [this](Event &event) { imChangeCallback(); }
+ [this](Event &event) {
+ FCITX_UNUSED(event);
+ imChangeCallback();
+ }
));
eventHandlers_.emplace_back(instance_->watchEvent(
- EventType::InputContextUpdateUI,
+ EventType::InputContextFlushUI,
EventWatcherPhase::Default,
[this](Event &event) {
- auto &e = static_cast(event);
+ auto &e = static_cast(event);
switch (e.component()) {
case UserInterfaceComponent::InputPanel: {
- auto *ic = dynamic_cast(activeIC_);
- if (ic) ic->updateInputPanel();
+ if (activeIC_) {
+ activeIC_->updateInputPanel();
+ if (pagingMode_ == 0) {
+ activeIC_->updateCandidatesBulk();
+ } else {
+ activeIC_->updateCandidatesPaged();
+ }
+ }
break;
}
case UserInterfaceComponent::StatusArea: {
- handleStatusAreaUpdate();
+ statusAreaUpdateCallback();
break;
}
}
@@ -184,10 +317,9 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
}
void AndroidFrontend::keyEvent(const Key &key, bool isRelease, const int timestamp) {
- auto *ic = activeIC_;
- if (!ic) return;
- KeyEvent keyEvent(ic, key, isRelease);
- ic->keyEvent(keyEvent);
+ if (!activeIC_) return;
+ KeyEvent keyEvent(activeIC_, key, isRelease);
+ activeIC_->keyEvent(keyEvent);
if (!keyEvent.accepted()) {
auto sym = key.sym();
keyEventCallback(sym, key.states(), Key::keySymToUnicode(sym), isRelease, timestamp);
@@ -199,8 +331,8 @@ void AndroidFrontend::forwardKey(const Key &key, bool isRelease) {
keyEventCallback(sym, key.states(), Key::keySymToUnicode(sym), isRelease, -1);
}
-void AndroidFrontend::commitString(const std::string &str) {
- commitStringCallback(str);
+void AndroidFrontend::commitString(const std::string &str, const int cursor) {
+ commitStringCallback(str, cursor);
}
void AndroidFrontend::updateCandidateList(const std::vector &candidates, const int size) {
@@ -212,7 +344,7 @@ void AndroidFrontend::updateClientPreedit(const Text &clientPreedit) {
}
void AndroidFrontend::updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown) {
- inputPanelAuxCallback(preedit, auxUp, auxDown);
+ inputPanelCallback(preedit, auxUp, auxDown);
}
void AndroidFrontend::releaseInputContext(const int uid) {
@@ -220,46 +352,53 @@ void AndroidFrontend::releaseInputContext(const int uid) {
}
bool AndroidFrontend::selectCandidate(int idx) {
- auto *ic = dynamic_cast(focusGroup_.focusedInputContext());
- if (!ic) return false;
- return ic->selectCandidate(idx);
+ if (!activeIC_) return false;
+ if (pagingMode_) {
+ return activeIC_->selectCandidatePaged(idx);
+ } else {
+ return activeIC_->selectCandidateBulk(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() {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return true;
- return ic->inputPanel().empty();
+ if (!activeIC_) return true;
+ return activeIC_->inputPanel().empty();
}
void AndroidFrontend::resetInputContext() {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return;
- ic->reset();
+ if (!activeIC_) return;
+ activeIC_->reset();
}
void AndroidFrontend::repositionCursor(int position) {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return;
- auto engine = instance_->inputMethodEngine(ic);
- InvokeActionEvent event(InvokeActionEvent::Action::LeftClick, position, ic);
- engine->invokeAction(*(instance_->inputMethodEntry(ic)), event);
+ if (!activeIC_) return;
+ InvokeActionEvent event(InvokeActionEvent::Action::LeftClick, position, activeIC_);
+ activeIC_->invokeAction(event);
}
void AndroidFrontend::focusInputContext(bool focus) {
+ if (!activeIC_) return;
if (focus) {
- if (!activeIC_) return;
activeIC_->focusIn();
} else {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return;
- ic->focusOut();
+ activeIC_->focusOut();
}
}
void AndroidFrontend::activateInputContext(const int uid, const std::string &pkgName) {
auto *ptr = icCache_.find(uid);
if (ptr) {
- activeIC_ = ptr->get();
+ activeIC_ = dynamic_cast(ptr->get());
} else {
auto *ic = new AndroidInputContext(this, instance_->inputContextManager(), uid, pkgName);
activeIC_ = ic;
@@ -289,9 +428,29 @@ void AndroidFrontend::setCandidateListCallback(const CandidateListCallback &call
}
std::vector AndroidFrontend::getCandidates(const int offset, const int limit) {
- auto *ic = dynamic_cast(focusGroup_.focusedInputContext());
- if (!ic) return {};
- return ic->getCandidates(offset, limit);
+ if (!activeIC_) return {};
+ return activeIC_->getCandidates(offset, limit);
+}
+
+void AndroidFrontend::deleteSurrounding(const int before, const int after) {
+ deleteSurroundingCallback(before, after);
+}
+
+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) {
@@ -303,7 +462,7 @@ void AndroidFrontend::setPreeditCallback(const ClientPreeditCallback &callback)
}
void AndroidFrontend::setInputPanelAuxCallback(const InputPanelCallback &callback) {
- inputPanelAuxCallback = callback;
+ inputPanelCallback = callback;
}
void AndroidFrontend::setKeyEventCallback(const KeyEventCallback &callback) {
@@ -318,15 +477,16 @@ void AndroidFrontend::setStatusAreaUpdateCallback(const StatusAreaUpdateCallback
statusAreaUpdateCallback = callback;
}
-void AndroidFrontend::handleStatusAreaUpdate() {
- if (statusAreaUpdated_) return;
- statusAreaUpdated_ = true;
- statusAreaDefer_ = instance_->eventLoop().addDeferEvent([this](EventSource *) {
- statusAreaUpdateCallback();
- statusAreaUpdated_ = false;
- statusAreaDefer_ = nullptr;
- return true;
- });
+void AndroidFrontend::setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback) {
+ deleteSurroundingCallback = callback;
+}
+
+void AndroidFrontend::setToastCallback(const ToastCallback &callback) {
+ toastCallback = callback;
+}
+
+void AndroidFrontend::setPagedCandidateCallback(const PagedCandidateCallback &callback) {
+ pagedCandidateCallback = callback;
}
class AndroidFrontendFactory : public AddonFactory {
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.h b/app/src/main/cpp/androidfrontend/androidfrontend.h
index a8bd64278..9b9db800b 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.h
@@ -1,9 +1,13 @@
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_H_
+/*
+ * 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
#include
#include
-#include
#include
#include "androidfrontend_public.h"
@@ -11,17 +15,20 @@
namespace fcitx {
+class AndroidInputContext;
+
class AndroidFrontend : public AddonInstance {
public:
- AndroidFrontend(Instance *instance);
+ explicit AndroidFrontend(Instance *instance);
Instance *instance() { return instance_; }
void updateCandidateList(const std::vector &candidates, const int size);
- void commitString(const std::string &str);
+ void commitString(const std::string &str, const int cursor);
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);
@@ -32,9 +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);
@@ -42,6 +55,9 @@ class AndroidFrontend : public AddonInstance {
void setKeyEventCallback(const KeyEventCallback &callback);
void setInputMethodChangeCallback(const InputMethodChangeCallback &callback);
void setStatusAreaUpdateCallback(const StatusAreaUpdateCallback &callback);
+ void setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback);
+ void setToastCallback(const ToastCallback &callback);
+ void setPagedCandidateCallback(const PagedCandidateCallback &callback);
private:
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, keyEvent);
@@ -55,6 +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);
@@ -62,25 +83,28 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setKeyEventCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setInputMethodChangeCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setStatusAreaUpdateCallback);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setToastCallback);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPagedCandidateCallback);
Instance *instance_;
FocusGroup focusGroup_;
- InputContext *activeIC_;
+ AndroidInputContext *activeIC_;
InputContextCache icCache_;
std::vector>> eventHandlers_;
- std::unique_ptr statusAreaDefer_;
- bool statusAreaUpdated_;
-
- void handleStatusAreaUpdate();
+ int pagingMode_;
CandidateListCallback candidateListCallback = [](const std::vector &, const int) {};
- CommitStringCallback commitStringCallback = [](const std::string &) {};
+ CommitStringCallback commitStringCallback = [](const std::string &, const int) {};
ClientPreeditCallback preeditCallback = [](const Text &) {};
- InputPanelCallback inputPanelAuxCallback = [](const fcitx::Text &, const fcitx::Text &, const Text &) {};
+ InputPanelCallback inputPanelCallback = [](const fcitx::Text &, const fcitx::Text &, const Text &) {};
KeyEventCallback keyEventCallback = [](const int, const uint32_t, const uint32_t, const bool, const int) {};
InputMethodChangeCallback imChangeCallback = [] {};
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 f75a72711..69bcbbdd5 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend_public.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
@@ -1,16 +1,27 @@
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+/*
+ * 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
#include
+#include
+#include
#include
+#include "../helper-types.h"
+
typedef std::function &, const int)> CandidateListCallback;
-typedef std::function CommitStringCallback;
+typedef std::function CommitStringCallback;
typedef std::function ClientPreeditCallback;
typedef std::function InputPanelCallback;
typedef std::function KeyEventCallback;
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))
@@ -45,6 +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 &))
@@ -66,4 +92,13 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setInputMethodChangeCallback,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setStatusAreaUpdateCallback,
void(const StatusAreaUpdateCallback &))
-#endif // _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback,
+ void(const DeleteSurroundingCallback &))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setToastCallback,
+ void(const ToastCallback &))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setPagedCandidateCallback,
+ void(const PagedCandidateCallback &))
+
+#endif // FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
diff --git a/app/src/main/cpp/androidfrontend/inputcontextcache.h b/app/src/main/cpp/androidfrontend/inputcontextcache.h
index 058044dce..637ecf44e 100644
--- a/app/src/main/cpp/androidfrontend/inputcontextcache.h
+++ b/app/src/main/cpp/androidfrontend/inputcontextcache.h
@@ -1,5 +1,9 @@
-// modified from https://github.com/fcitx/libime/blob/1.0.14/src/libime/core/lrucache.h
-
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2017-2017 CSSlayer
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileComment: Modified from https://github.com/fcitx/libime/blob/1.0.14/src/libime/core/lrucache.h
+ */
#ifndef FCITX5_ANDROID_INPUTCONTEXTCACHE_H
#define FCITX5_ANDROID_INPUTCONTEXTCACHE_H
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in b/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in
index 7f83929d9..03d617017 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in
@@ -8,5 +8,4 @@ Configurable=True
[Addon/OptionalDependencies]
0=spell
-1=quickphrase
-;2=emoji
\ No newline at end of file
+1=quickphrase
\ No newline at end of file
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
index 6c517cc94..a84d55ab7 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#include
#include
#include
@@ -10,8 +14,6 @@
#include "androidkeyboard.h"
-#define FCITX_KEYBOARD_MAX_BUFFER 20
-
namespace fcitx {
namespace {
@@ -85,7 +87,7 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
// check if we can select candidate.
if (auto candList = inputContext->inputPanel().candidateList()) {
- int idx = key.keyListIndex(selectionKeys_);
+ const int idx = key.keyListIndex(selectionKeys_);
if (idx >= 0 && idx < candList->size()) {
event.filterAndAccept();
candList->candidate(idx).select(inputContext);
@@ -93,9 +95,9 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
}
}
- bool validSym = isValidSym(key);
+ const bool validSym = isValidSym(key);
- static KeyList FCITX_HYPHEN_APOS = {Key(FcitxKey_minus), Key(FcitxKey_apostrophe)};
+ static const KeyList FCITX_HYPHEN_APOS = {Key(FcitxKey_minus), Key(FcitxKey_apostrophe)};
// check for valid character
if (key.isSimple() || validSym) {
// prepend space before input next word
@@ -106,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();
}
}
@@ -119,7 +120,7 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
}
return updateCandidate(entry, inputContext);
}
- } else if (key.check(FcitxKey_Delete)) {
+ } else if (key.check(FcitxKey_Delete) || key.check(FcitxKey_KP_Delete)) {
if (buffer.del()) {
event.filterAndAccept();
if (buffer.empty()) {
@@ -140,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);
}
}
@@ -173,7 +174,7 @@ std::vector AndroidKeyboardEngine::listInputMethods() {
void AndroidKeyboardEngine::reloadConfig() {
readAsIni(config_, ConfPath);
selectionKeys_.clear();
- KeySym syms[] = {
+ const std::array syms{
FcitxKey_1, FcitxKey_2, FcitxKey_3, FcitxKey_4, FcitxKey_5,
FcitxKey_6, FcitxKey_7, FcitxKey_8, FcitxKey_9, FcitxKey_0,
};
@@ -209,6 +210,7 @@ void AndroidKeyboardEngine::setConfig(const RawConfig &config) {
}
void AndroidKeyboardEngine::activate(const InputMethodEntry &entry, InputContextEvent &event) {
+ FCITX_UNUSED(entry);
auto *inputContext = event.inputContext();
wordHintAction_.setChecked(*config_.enableWordHint);
wordHintAction_.update(inputContext);
@@ -226,6 +228,7 @@ void AndroidKeyboardEngine::deactivate(const InputMethodEntry &entry, InputConte
}
void AndroidKeyboardEngine::reset(const InputMethodEntry &entry, InputContextEvent &event) {
+ FCITX_UNUSED(entry);
auto *inputContext = event.inputContext();
resetState(inputContext);
inputContext->inputPanel().reset();
@@ -245,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(),
- 20);
+ 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);
}
@@ -265,45 +276,46 @@ void AndroidKeyboardEngine::updateCandidate(const InputMethodEntry &entry, Input
}
void AndroidKeyboardEngine::updateUI(InputContext *inputContext) {
- auto *state = inputContext->propertyFor(&factory_);
- Text preedit(preeditString(inputContext), TextFormatFlag::Underline);
- preedit.setCursor(static_cast(state->buffer_.cursorByChar()));
- inputContext->inputPanel().setClientPreedit(preedit);
- // we don't want preedit here ...
-// if (!inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) {
-// inputContext->inputPanel().setPreedit(preedit);
-// }
- inputContext->updatePreedit();
+ auto [text, cursor] = preeditWithCursor(inputContext);
+ if (inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) {
+ Text clientPreedit(text, TextFormatFlag::Underline);
+ clientPreedit.setCursor(static_cast(cursor));
+ inputContext->inputPanel().setClientPreedit(clientPreedit);
+ inputContext->updatePreedit();
+ } else {
+ Text preedit(text);
+ preedit.setCursor(static_cast(cursor));
+ inputContext->inputPanel().setPreedit(preedit);
+ }
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;
}
auto *state = inputContext->propertyFor(&factory_);
- const CapabilityFlags noPredictFlag{CapabilityFlag::Password,
- CapabilityFlag::NoSpellCheck,
- CapabilityFlag::Sensitive};
- // no spell hint enabled or no supported dictionary
+ // word hint is disabled, input is password, or language not supported
if (!*config_.enableWordHint ||
- inputContext->capabilityFlags().testAny(noPredictFlag) ||
+ (!*config_.hintOnPhysicalKeyboard && !event.isVirtual()) ||
+ (*config_.editorControlledWordHint && inputContext->capabilityFlags().test(CapabilityFlag::NoSpellCheck)) ||
+ inputContext->capabilityFlags().test(CapabilityFlag::Password) ||
!supportHint(entry->languageCode())) {
return false;
}
auto &buffer = state->buffer_;
- auto preedit = preeditString(inputContext);
+ auto [preedit, cursor] = preeditWithCursor(inputContext);
if (preedit != buffer.userInput()) {
buffer.clear();
buffer.type(preedit);
}
- buffer.type(chr);
+ buffer.type(Key::keySymToUTF8(event.key().sym()));
- if (buffer.size() >= FCITX_KEYBOARD_MAX_BUFFER) {
+ if (buffer.size() >= MaxBufferSize) {
commitBuffer(inputContext);
return true;
}
@@ -313,11 +325,16 @@ bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::
}
void AndroidKeyboardEngine::commitBuffer(InputContext *inputContext) {
- auto preedit = preeditString(inputContext);
+ auto [preedit, cursor] = preeditWithCursor(inputContext);
if (preedit.empty()) {
return;
}
- inputContext->commitString(preedit);
+ auto characterCount = utf8::length(preedit, 0, cursor);
+ if (inputContext->capabilityFlags().test(CapabilityFlag::CommitStringWithCursor)) {
+ inputContext->commitStringWithCursor(preedit, characterCount);
+ } else {
+ inputContext->commitString(preedit);
+ }
resetState(inputContext);
inputContext->inputPanel().reset();
inputContext->updatePreedit();
@@ -329,22 +346,22 @@ bool AndroidKeyboardEngine::supportHint(const std::string &language) {
return hasSpell;
}
-std::string AndroidKeyboardEngine::preeditString(InputContext *inputContext) {
+std::pair AndroidKeyboardEngine::preeditWithCursor(InputContext *inputContext) {
auto *state = inputContext->propertyFor(&factory_);
- return state->buffer_.userInput();
+ return {state->buffer_.userInput(), state->buffer_.cursorByChar()};
}
void AndroidKeyboardEngine::invokeActionImpl(const InputMethodEntry &entry, InvokeActionEvent &event) {
- size_t cursor = event.cursor();
+ const int cursor = event.cursor();
auto inputContext = event.inputContext();
auto *state = inputContext->propertyFor(&factory_);
if (event.action() != InvokeActionEvent::Action::LeftClick
|| cursor < 0
- || cursor > state->buffer_.size()) {
+ || static_cast(cursor) > state->buffer_.size()) {
return InputMethodEngineV3::invokeActionImpl(entry, event);
}
event.filter();
- state->buffer_.setCursor(event.cursor());
+ state->buffer_.setCursor(static_cast(cursor));
updateUI(inputContext);
}
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.h b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
index 847b64fc7..b365ac527 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.h
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
@@ -1,5 +1,9 @@
-#ifndef _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
-#define _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
+/*
+ * 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
#include
#include
@@ -26,6 +30,10 @@ 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
pageSize{this, "PageSize", _("Word hint page size"), 5, IntConstrain(3, 10)};
OptionWithAnnotation
@@ -50,8 +58,10 @@ struct AndroidKeyboardEngineState : public InputContextProperty {
class AndroidKeyboardEngine final : public InputMethodEngineV3 {
public:
- AndroidKeyboardEngine(Instance *instance);
- ~AndroidKeyboardEngine() = default;
+ static int constexpr MaxBufferSize = 20;
+ static int constexpr SpellCandidateSize = 20;
+
+ explicit AndroidKeyboardEngine(Instance *instance);
Instance *instance() { return instance_; }
@@ -87,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().
@@ -100,7 +110,10 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
private:
bool supportHint(const std::string &language);
- std::string preeditString(InputContext *inputContext);
+ /**
+ * preedit string and byte cursor
+ */
+ std::pair preeditWithCursor(InputContext *inputContext);
Instance *instance_;
AndroidKeyboardEngineConfig config_;
@@ -121,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/CMakeLists.txt b/app/src/main/cpp/androidnotification/CMakeLists.txt
new file mode 100644
index 000000000..86d583495
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/CMakeLists.txt
@@ -0,0 +1,10 @@
+add_definitions(-DFCITX_GETTEXT_DOMAIN=\"fcitx5-android\")
+
+add_library(androidnotification MODULE androidnotification.cpp)
+target_link_libraries(androidnotification Fcitx5::Core Fcitx5::Utils Fcitx5::Module::Notifications)
+
+configure_file(notifications.conf.in.in notifications.conf.in @ONLY)
+fcitx5_translate_desktop_file(${CMAKE_CURRENT_BINARY_DIR}/notifications.conf.in notifications.conf)
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/notifications.conf"
+ DESTINATION "${FCITX_INSTALL_PKGDATADIR}/addon"
+ COMPONENT config)
diff --git a/app/src/main/cpp/androidnotification/androidnotification.cpp b/app/src/main/cpp/androidnotification/androidnotification.cpp
new file mode 100644
index 000000000..b2e06b5e4
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/androidnotification.cpp
@@ -0,0 +1,100 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
+ */
+#include
+
+#include
+#include
+
+#include "../androidfrontend/androidfrontend_public.h"
+
+#include "androidnotification.h"
+
+namespace fcitx {
+
+Notifications::Notifications(Instance *instance) : instance_(instance) {
+ reloadConfig();
+}
+
+void Notifications::reloadConfig() {
+ readAsIni(config_, ConfPath);
+ updateHiddenNotifications();
+}
+
+void Notifications::save() {
+ std::vector values_;
+ values_.reserve(hiddenNotifications_.size());
+ for (const auto &id: hiddenNotifications_) {
+ values_.push_back(id);
+ }
+ config_.hiddenNotifications.setValue(std::move(values_));
+ safeSaveAsIni(config_, ConfPath);
+}
+
+void Notifications::setConfig(const fcitx::RawConfig &config) {
+ config_.load(config, true);
+ safeSaveAsIni(config_, ConfPath);
+ updateHiddenNotifications();
+}
+
+void Notifications::updateHiddenNotifications() {
+ hiddenNotifications_.clear();
+ for (const auto &id: config_.hiddenNotifications.value()) {
+ hiddenNotifications_.insert(id);
+ }
+}
+
+uint32_t Notifications::sendNotification(
+ const std::string &appName,
+ uint32_t replaceId,
+ const std::string &appIcon,
+ const std::string &summary,
+ const std::string &body,
+ const std::vector &actions,
+ int32_t timeout,
+ NotificationActionCallback actionCallback,
+ NotificationClosedCallback closedCallback) {
+ // TODO implement Notification
+ FCITX_UNUSED(appName);
+ FCITX_UNUSED(replaceId);
+ FCITX_UNUSED(appIcon);
+ FCITX_UNUSED(summary);
+ FCITX_UNUSED(body);
+ FCITX_UNUSED(actions);
+ FCITX_UNUSED(timeout);
+ FCITX_UNUSED(actionCallback);
+ FCITX_UNUSED(closedCallback);
+ return 0;
+}
+
+void Notifications::showTip(
+ const std::string &tipId,
+ const std::string &appName,
+ const std::string &appIcon,
+ const std::string &summary,
+ const std::string &body,
+ int32_t timeout) {
+ FCITX_UNUSED(appName);
+ FCITX_UNUSED(appIcon);
+ FCITX_UNUSED(timeout);
+ if (hiddenNotifications_.count(tipId)) {
+ return;
+ }
+ std::string const s = summary + ": " + body;
+ androidfrontend()->call(s);
+}
+
+void Notifications::closeNotification(uint64_t internalId) {
+ FCITX_UNUSED(internalId);
+}
+
+class NotificationsModuleFactory : public AddonFactory {
+ AddonInstance *create(AddonManager *manager) override {
+ return new Notifications(manager->instance());
+ }
+};
+
+}
+
+FCITX_ADDON_FACTORY(fcitx::NotificationsModuleFactory)
diff --git a/app/src/main/cpp/androidnotification/androidnotification.h b/app/src/main/cpp/androidnotification/androidnotification.h
new file mode 100644
index 000000000..242e2857b
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/androidnotification.h
@@ -0,0 +1,76 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
+ */
+#ifndef FCITX5_ANDROID_ANDROIDNOTIFICATION_H
+#define FCITX5_ANDROID_ANDROIDNOTIFICATION_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace fcitx {
+
+FCITX_CONFIGURATION(NotificationsConfig,
+ fcitx::Option> hiddenNotifications{
+ this, "HiddenNotifications",
+ _("Hidden Notifications")};)
+
+class Notifications final : public AddonInstance {
+public:
+ explicit Notifications(Instance *instance);
+
+ Instance *instance() { return instance_; }
+
+ void reloadConfig() override;
+
+ void save() override;
+
+ const Configuration *getConfig() const override { return &config_; }
+
+ void setConfig(const RawConfig &config) override;
+
+ FCITX_ADDON_DEPENDENCY_LOADER(androidfrontend, instance_->addonManager());
+
+ uint32_t sendNotification(const std::string &appName, uint32_t replaceId,
+ const std::string &appIcon,
+ const std::string &summary,
+ const std::string &body,
+ const std::vector &actions,
+ int32_t timeout,
+ NotificationActionCallback actionCallback,
+ NotificationClosedCallback closedCallback);
+
+ void showTip(const std::string &tipId, const std::string &appName,
+ const std::string &appIcon, const std::string &summary,
+ const std::string &body, int32_t timeout);
+
+ void closeNotification(uint64_t internalId);
+
+private:
+ FCITX_ADDON_EXPORT_FUNCTION(Notifications, sendNotification);
+ FCITX_ADDON_EXPORT_FUNCTION(Notifications, showTip);
+ FCITX_ADDON_EXPORT_FUNCTION(Notifications, closeNotification);
+
+ static const inline char* ConfPath = "conf/androidnotification.conf";
+
+ NotificationsConfig config_;
+ Instance *instance_;
+
+ std::unordered_set hiddenNotifications_;
+
+ void updateHiddenNotifications();
+
+}; // class Notifications
+
+} // namespace fcitx
+
+#endif //FCITX5_ANDROID_ANDROIDNOTIFICATION_H
diff --git a/app/src/main/cpp/androidnotification/notifications.conf.in.in b/app/src/main/cpp/androidnotification/notifications.conf.in.in
new file mode 100644
index 000000000..453050f16
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/notifications.conf.in.in
@@ -0,0 +1,11 @@
+[Addon]
+Name=Android Toast & Notification
+Type=SharedLibrary
+Library=libandroidnotification
+Category=Module
+Version=@PROJECT_VERSION@
+OnDemand=True
+Configurable=True
+
+[Addon/Dependencies]
+0=androidfrontend:@PROJECT_VERSION@
diff --git a/app/src/main/cpp/fcitx5-chinese-addons b/app/src/main/cpp/fcitx5-chinese-addons
deleted file mode 160000
index 21e416db6..000000000
--- a/app/src/main/cpp/fcitx5-chinese-addons
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 21e416db60485a1871a169285f7a5cbb774d84bf
diff --git a/app/src/main/cpp/fcitx5-lua b/app/src/main/cpp/fcitx5-lua
deleted file mode 160000
index d8a1319c6..000000000
--- a/app/src/main/cpp/fcitx5-lua
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d8a1319c6583b0055b4bd4ea5bbd0292a8f8023a
diff --git a/app/src/main/cpp/fcitx5-unikey b/app/src/main/cpp/fcitx5-unikey
deleted file mode 160000
index 0f2867136..000000000
--- a/app/src/main/cpp/fcitx5-unikey
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0f2867136b130967da183bb21921defd60c0a17c
diff --git a/app/src/main/cpp/helper-types.h b/app/src/main/cpp/helper-types.h
index fa2e9453e..1f13c9494 100644
--- a/app/src/main/cpp/helper-types.h
+++ b/app/src/main/cpp/helper-types.h
@@ -1,30 +1,51 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#ifndef FCITX5_ANDROID_HELPER_TYPES_H
#define FCITX5_ANDROID_HELPER_TYPES_H
#include
#include
#include
+#include
+#include
+#include
+#include
+
+#include
class InputMethodStatus {
public:
- const fcitx::InputMethodEntry *entry;
+ // fcitx::InputMethodEntry
+ std::string uniqueName;
+ std::string name;
+ std::string nativeName;
+ std::string icon;
+ std::string label;
+ std::string languageCode;
+ std::string addon;
+ bool configurable = false;
+ // fcitx::InputMethodEngine
std::string subMode;
std::string subModeLabel;
std::string subModeIcon;
InputMethodStatus(const fcitx::InputMethodEntry *entry,
fcitx::InputMethodEngine *engine,
- fcitx::InputContext *ic)
- : entry(entry) {
- if (engine) {
- subMode = engine->subMode(*entry, *ic);
- subModeLabel = engine->subModeLabel(*entry, *ic);
- subModeIcon = engine->subModeIcon(*entry, *ic);
- }
+ fcitx::InputContext *ic) {
+ uniqueName = entry->uniqueName();
+ name = entry->name();
+ nativeName = entry->nativeName();
+ icon = entry->icon();
+ label = entry->label();
+ languageCode = entry->languageCode();
+ addon = entry->addon();
+ configurable = entry->isConfigurable();
+ subMode = engine->subMode(*entry, *ic);
+ subModeLabel = engine->subModeLabel(*entry, *ic);
+ subModeIcon = engine->subModeIcon(*entry, *ic);
}
-
- InputMethodStatus(const fcitx::InputMethodEntry *entry)
- : entry(entry) {}
};
class AddonStatus {
@@ -68,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 5ec574728..6807b531d 100644
--- a/app/src/main/cpp/jni-utils.h
+++ b/app/src/main/cpp/jni-utils.h
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#ifndef FCITX5_ANDROID_JNI_UTILS_H
#define FCITX5_ANDROID_JNI_UTILS_H
@@ -73,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);
}
@@ -102,6 +106,7 @@ class GlobalRefSingleton {
jmethodID BooleanInit;
jclass Fcitx;
+ jmethodID ShowToast;
jmethodID HandleFcitxEvent;
jclass InputMethodEntry;
@@ -127,7 +132,19 @@ class GlobalRefSingleton {
jclass FormattedText;
jmethodID FormattedTextFromByteCursor;
- GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
+ jclass PinyinCustomPhrase;
+ jmethodID PinyinCustomPhraseInit;
+ jfieldID PinyinCustomPhraseKey;
+ jfieldID PinyinCustomPhraseOrder;
+ jfieldID PinyinCustomPhraseValue;
+
+ jclass CandidateAction;
+ jmethodID CandidateActionInit;
+
+ jclass Candidate;
+ jmethodID CandidateInit;
+
+ explicit GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
JNIEnv *env;
jvm->AttachCurrentThread(&env, nullptr);
@@ -142,11 +159,12 @@ class GlobalRefSingleton {
BooleanInit = env->GetMethodID(Boolean, "", "(Z)V");
Fcitx = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/Fcitx")));
+ ShowToast = env->GetStaticMethodID(Fcitx, "showToast", "(Ljava/lang/String;)V");
HandleFcitxEvent = env->GetStaticMethodID(Fcitx, "handleFcitxEvent", "(I[Ljava/lang/Object;)V");
InputMethodEntry = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/InputMethodEntry")));
- InputMethodEntryInit = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V");
- InputMethodEntryInitWithSubMode = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
+ InputMethodEntryInit = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V");
+ InputMethodEntryInitWithSubMode = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
RawConfig = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/RawConfig")));
RawConfigName = env->GetFieldID(RawConfig, "name", "Ljava/lang/String;");
@@ -166,9 +184,21 @@ class GlobalRefSingleton {
FormattedText = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/FormattedText")));
FormattedTextFromByteCursor = env->GetStaticMethodID(FormattedText, "fromByteCursor", "([Ljava/lang/String;[II)Lorg/fcitx/fcitx5/android/core/FormattedText;");
+
+ PinyinCustomPhrase = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/data/pinyin/customphrase/PinyinCustomPhrase")));
+ PinyinCustomPhraseInit = env->GetMethodID(PinyinCustomPhrase, "", "(Ljava/lang/String;ILjava/lang/String;)V");
+ 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/libime b/app/src/main/cpp/libime
deleted file mode 160000
index ba001dc0a..000000000
--- a/app/src/main/cpp/libime
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ba001dc0a3aeaf7e253fef9b7dd41cc0736e1944
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
index bfb90acd4..bdeb17191 100644
--- a/app/src/main/cpp/native-lib.cpp
+++ b/app/src/main/cpp/native-lib.cpp
@@ -1,12 +1,18 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ */
#include
+#include
+
#include
#include
#include
#include
-#include
+#include
#include
#include
@@ -19,7 +25,9 @@
#include
#include
#include
+#include
#include
+#include
#include
#include
@@ -28,6 +36,10 @@
#include
#include
+#include
+#include
+#include "customphrase.h"
+
#include "androidfrontend/androidfrontend_public.h"
#include "jni-utils.h"
#include "nativestreambuf.h"
@@ -59,19 +71,17 @@ class Fcitx {
return p_instance != nullptr && p_dispatcher != nullptr && p_frontend != nullptr;
}
- event_base *get_event_base() {
+ uv_loop_t *get_event_base() {
fcitx::EventLoop &event_loop = p_instance->eventLoop();
- return static_cast(event_loop.nativeHandle());
+ return static_cast(event_loop.nativeHandle());
}
int loopOnce() {
- return event_base_loop(get_event_base(), EVLOOP_ONCE);
+ return uv_run(get_event_base(), UV_RUN_ONCE);
}
void startup(const std::function &setupCallback) {
- char arg0[] = "";
- char *argv[] = {arg0};
- p_instance = std::make_unique(FCITX_ARRAY_SIZE(argv), argv);
+ p_instance = std::make_unique(0, nullptr);
p_instance->addonManager().registerDefaultLoader(nullptr);
p_dispatcher = std::make_unique();
p_dispatcher->attach(&p_instance->eventLoop());
@@ -136,17 +146,15 @@ class Fcitx {
const auto *entry = imMgr.entry(ime.name());
entries.emplace_back(entry);
}
- return std::move(entries);
+ return entries;
}
- InputMethodStatus inputMethodStatus() {
+ std::unique_ptr inputMethodStatus() {
auto *ic = p_frontend->call();
- auto *engine = p_instance->inputMethodEngine(ic);
- const auto *entry = p_instance->inputMethodEntry(ic);
- if (engine) {
- return {entry, engine, ic};
- }
- return {entry};
+ if (!ic) return nullptr;
+ auto *entry = p_instance->inputMethodEntry(ic);
+ auto *engine = static_cast(p_instance->addonManager().addon(entry->addon(), true));
+ return std::make_unique(entry, engine, ic);
}
void setInputMethod(const std::string &ime) {
@@ -163,7 +171,7 @@ class Fcitx {
entries.emplace_back(&entry);
return true;
});
- return std::move(entries);
+ return entries;
}
void setEnabledInputMethods(std::vector &entries) {
@@ -279,9 +287,9 @@ class Fcitx {
auto &globalConfig = p_instance->globalConfig();
auto &addonManager = p_instance->addonManager();
const auto &enabledAddons = globalConfig.enabledAddons();
- std::unordered_set enabledSet(enabledAddons.begin(), enabledAddons.end());
+ const std::unordered_set enabledSet(enabledAddons.begin(), enabledAddons.end());
const auto &disabledAddons = globalConfig.disabledAddons();
- std::unordered_set
+ const std::unordered_set
disabledSet(disabledAddons.begin(), disabledAddons.end());
std::vector addons;
for (const auto category: {fcitx::AddonCategory::InputMethod,
@@ -301,7 +309,7 @@ class Fcitx {
} else if (enabledSet.count(info->uniqueName())) {
enabled = true;
}
- addons.emplace_back(AddonStatus(info, enabled));
+ addons.emplace_back(info, enabled);
}
}
return addons;
@@ -354,9 +362,9 @@ class Fcitx {
p_unicode->call(ic);
}
- void setClipboard(const std::string &string) {
+ void setClipboard(const std::string &string, bool password) {
if (!p_clipboard) return;
- p_clipboard->call("", string);
+ p_clipboard->call("", string, password);
}
void focusInputContext(bool focus) {
@@ -387,7 +395,7 @@ class Fcitx {
fcitx::StatusGroup::InputMethod,
fcitx::StatusGroup::AfterInputMethod}) {
for (auto act: ic->statusArea().actions(group)) {
- actions.emplace_back(ActionEntity(act, ic));
+ actions.emplace_back(act, ic);
}
}
return actions;
@@ -405,13 +413,33 @@ 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();
}
void exit() {
// Make sure that the exec doesn't get blocked
- event_base_loopexit(get_event_base(), nullptr);
+ uv_stop(get_event_base());
// Normally, we would use exec to drive the event loop.
// Since we are calling loopOnce in JVM repeatedly, we shouldn't have used this function.
// However, exit events would lose chance to be called in this case.
@@ -461,6 +489,9 @@ JNI_OnLoad(JavaVM *jvm, void * /* reserved */) {
return JNI_VERSION_1_6;
}
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_setupLogStream(JNIEnv *env, jclass clazz, jboolean verbose) {
@@ -471,40 +502,47 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setupLogStream(JNIEnv *env, jclass claz
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz, jstring locale, jstring appData, jstring appLib, jstring extData, jstring extCache, jobjectArray extDomains) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
+ JNIEnv *env, jclass clazz,
+ jstring locale,
+ jstring appData,
+ jstring appLib,
+ jstring extData,
+ jstring extCache,
+ jobjectArray extDomains) {
if (Fcitx::Instance().isRunning()) {
FCITX_ERROR() << "Fcitx is already running!";
return;
}
FCITX_INFO() << "Starting...";
- setenv("SKIP_FCITX_PATH", "true", 1);
-
auto locale_ = CString(env, locale);
auto appData_ = CString(env, appData);
auto appLib_ = CString(env, appLib);
auto extData_ = CString(env, extData);
auto extCache_ = CString(env, extCache);
- std::string lang_ = fcitx::stringutils::split(*locale_, ":")[0];
- std::string config_home = fcitx::stringutils::joinPath(*extData_, "config");
- std::string data_home = fcitx::stringutils::joinPath(*extData_, "data");
- std::string usr_share = fcitx::stringutils::joinPath(*appData_, "usr", "share");
- std::string locale_dir = fcitx::stringutils::joinPath(usr_share, "locale");
- std::string libime_data = fcitx::stringutils::joinPath(usr_share, "libime");
- std::string lua_path = fcitx::stringutils::concat(
+ const std::string lang_ = fcitx::stringutils::split(*locale_, ":")[0];
+ const std::string config_home = fcitx::stringutils::joinPath(*extData_, "config");
+ const std::string data_home = fcitx::stringutils::joinPath(*extData_, "data");
+ const std::string usr_share = fcitx::stringutils::joinPath(*appData_, "usr", "share");
+ const std::string locale_dir = fcitx::stringutils::joinPath(usr_share, "locale");
+ const std::string libime_data = fcitx::stringutils::joinPath(usr_share, "libime");
+ const std::string lua_path = fcitx::stringutils::concat(
fcitx::stringutils::joinPath(data_home, "lua", "?.lua"), ";",
fcitx::stringutils::joinPath(data_home, "lua", "?", "init.lua"), ";",
fcitx::stringutils::joinPath(usr_share, "lua", "5.4", "?.lua"), ";",
fcitx::stringutils::joinPath(usr_share, "lua", "5.4", "?", "init.lua"), ";",
";" // double semicolon, for default path defined in luaconf.h
);
- std::string lua_cpath = fcitx::stringutils::concat(
+ const std::string lua_cpath = fcitx::stringutils::concat(
fcitx::stringutils::joinPath(data_home, "lua", "?.so"), ";",
fcitx::stringutils::joinPath(usr_share, "lua", "5.4", "?.so"), ";",
";"
);
+ // prevent StandardPath from resolving it's hardcoded installation path
+ setenv("SKIP_FCITX_PATH", "1", 1);
// for fcitx default profile [DefaultInputMethod]
setenv("LANG", lang_.c_str(), 1);
// for libintl-lite loading gettext .mo translations
@@ -535,12 +573,11 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
const char *locale_dir_char = locale_dir.c_str();
fcitx::registerDomain("fcitx5", locale_dir_char);
- fcitx::registerDomain("fcitx5-chinese-addons", locale_dir_char);
fcitx::registerDomain("fcitx5-lua", locale_dir_char);
- fcitx::registerDomain("fcitx5-unikey", locale_dir_char);
+ fcitx::registerDomain("fcitx5-chinese-addons", locale_dir_char);
fcitx::registerDomain("fcitx5-android", locale_dir_char);
- int extDomainsSize = env->GetArrayLength(extDomains);
+ const int extDomainsSize = env->GetArrayLength(extDomains);
for (int i = 0; i < extDomainsSize; i++) {
auto domain = JRef(env, env->GetObjectArrayElement(extDomains, i));
fcitx::registerDomain(CString(env, domain), locale_dir_char);
@@ -559,10 +596,12 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
env->SetObjectArrayElement(vararg, 1, *candidatesArray);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 0, *vararg);
};
- auto commitStringCallback = [](const std::string &str) {
+ auto commitStringCallback = [](const std::string &str, const int cursor) {
auto env = GlobalRef->AttachEnv();
- auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->String, nullptr));
+ auto stringCursor = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, cursor));
+ auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->Object, nullptr));
env->SetObjectArrayElement(vararg, 0, JString(env, str));
+ env->SetObjectArrayElement(vararg, 1, stringCursor);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 1, *vararg);
};
auto preeditCallback = [](const fcitx::Text &clientPreedit) {
@@ -595,24 +634,71 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 5, *vararg);
};
auto imChangeCallback = []() {
+ std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
+ if (!status) return;
auto env = GlobalRef->AttachEnv();
auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
- const auto status = Fcitx::Instance().inputMethodStatus();
- auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, status));
+ 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();
- auto vararg = JRef(env, env->NewObjectArray(static_cast(actions.size()), GlobalRef->Action, nullptr));
+ auto actionArray = JRef(env, env->NewObjectArray(static_cast(actions.size()), GlobalRef->Action, nullptr));
int i = 0;
for (const auto &a: actions) {
auto obj = JRef(env, fcitxActionToJObject(env, a));
- env->SetObjectArrayElement(vararg, i++, obj);
+ env->SetObjectArrayElement(actionArray, i++, obj);
}
+ env->SetObjectArrayElement(vararg, 0, actionArray);
+ auto statusObj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
+ env->SetObjectArrayElement(vararg, 1, statusObj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 7, *vararg);
};
+ auto deleteSurroundingCallback = [](const int before, const int after) {
+ std::array arr{before, after};
+ auto env = GlobalRef->AttachEnv();
+ auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
+ auto intArray = JRef(env, env->NewIntArray(2));
+ env->SetIntArrayRegion(intArray, 0, 2, arr.data());
+ 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::StandardPaths::global().syncUmask();
Fcitx::Instance().startup([&](auto *androidfrontend) {
FCITX_INFO() << "Setting up callback";
@@ -624,6 +710,9 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
androidfrontend->template call(keyEventCallback);
androidfrontend->template call(imChangeCallback);
androidfrontend->template call(statusAreaUpdateCallback);
+ androidfrontend->template call(deleteSurroundingCallback);
+ androidfrontend->template call(pagedCandidateCallback);
+ androidfrontend->template call(toastCallback);
});
FCITX_INFO() << "Finishing startup";
}
@@ -658,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
- fcitx::Key parsedKey{fcitx::Key::keySymFromString((const char *) &c),
- fcitx::KeyStates(static_cast(state))};
+ const fcitx::Key parsedKey{fcitx::Key::keySymFromString(reinterpret_cast(&c)),
+ 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);
}
@@ -687,7 +779,6 @@ 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);
}
@@ -738,8 +829,9 @@ extern "C"
JNIEXPORT jobject JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_inputMethodStatus(JNIEnv *env, jclass clazz) {
RETURN_VALUE_IF_NOT_RUNNING(nullptr)
- const auto &status = Fcitx::Instance().inputMethodStatus();
- return fcitxInputMethodStatusToJObject(env, status);
+ auto status = Fcitx::Instance().inputMethodStatus();
+ if (!status) return nullptr;
+ return fcitxInputMethodStatusToJObject(env, *status);
}
extern "C"
@@ -884,9 +976,9 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_triggerUnicodeInput(JNIEnv *env, jclass
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxClipboard(JNIEnv *env, jclass clazz, jstring string) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxClipboard(JNIEnv *env, jclass clazz, jstring string, jboolean password) {
RETURN_IF_NOT_RUNNING
- Fcitx::Instance().setClipboard(CString(env, string));
+ Fcitx::Instance().setClipboard(CString(env, string), password == JNI_TRUE);
}
extern "C"
@@ -952,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) {
@@ -1041,3 +1168,91 @@ Java_org_fcitx_fcitx5_android_data_table_TableManager_checkTableDictFormat(JNIEn
}
return JNI_TRUE;
}
+
+extern "C"
+JNIEXPORT jobjectArray JNICALL
+Java_org_fcitx_fcitx5_android_data_pinyin_CustomPhraseManager_load(JNIEnv *env, jclass clazz) {
+ auto fp = fcitx::StandardPaths::global().open(fcitx::StandardPathsType::PkgData, "pinyin/customphrase");
+ if (fp.fd() < 0) {
+ FCITX_INFO() << "cannot open pinyin/customphrase";
+ return nullptr;
+ }
+ boost::iostreams::stream_buffer
+ buffer(fp.fd(), boost::iostreams::file_descriptor_flags::never_close_handle);
+ std::istream in(&buffer);
+ fcitx::CustomPhraseDict dict;
+ dict.load(in, true);
+ int size = 0;
+ dict.foreach([&](const std::string &key, std::vector &items) {
+ FCITX_UNUSED(key);
+ size += static_cast(items.size());
+ });
+ int i = 0;
+ jobjectArray array = env->NewObjectArray(size, GlobalRef->PinyinCustomPhrase, nullptr);
+ dict.foreach([&](const std::string &key, std::vector &items) {
+ for (const auto &item: items) {
+ env->SetObjectArrayElement(array, i++,
+ JRef(env, env->NewObject(GlobalRef->PinyinCustomPhrase, GlobalRef->PinyinCustomPhraseInit,
+ *JString(env, key),
+ item.order(),
+ *JString(env, item.value())
+ )
+ )
+ );
+ }
+ });
+ return array;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_data_pinyin_CustomPhraseManager_save(JNIEnv *env, jclass clazz, jobjectArray items) {
+ fcitx::CustomPhraseDict dict;
+ const int size = env->GetArrayLength(items);
+ for (int i = 0; i < size; i++) {
+ auto phrase = JRef(env, env->GetObjectArrayElement(items, i));
+ auto phraseKey = JRef(env, env->GetObjectField(phrase, GlobalRef->PinyinCustomPhraseKey));
+ auto phraseOrder = env->GetIntField(phrase, GlobalRef->PinyinCustomPhraseOrder);
+ auto phraseValue = JRef(env, env->GetObjectField(phrase, GlobalRef->PinyinCustomPhraseValue));
+ dict.addPhrase(*CString(env, phraseKey),
+ *CString(env, phraseValue),
+ static_cast(phraseOrder));
+ }
+ 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);
+ std::ostream out(&buffer);
+ dict.save(out);
+ return true;
+ });
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_org_fcitx_fcitx5_android_utils_Ini_readFromIni(JNIEnv *env, jclass clazz, jstring src) {
+ fcitx::RawConfig config;
+ FILE *fp = std::fopen(*CString(env, src), "rb");
+ if (!fp) {
+ return nullptr;
+ }
+ fcitx::readFromIni(config, fp);
+ std::fclose(fp);
+ return fcitxRawConfigToJObject(env, config);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_utils_Ini_writeAsIni(JNIEnv *env, jclass clazz, jstring dest, jobject value) {
+ FILE *fp = std::fopen(*CString(env, dest), "wb");
+ if (!fp) {
+ throwJavaException(env, "Unable to open file");
+ return;
+ }
+ auto config = jobjectToRawConfig(env, value);
+ fcitx::writeAsIni(config, fp);
+ std::fclose(fp);
+}
+
+#pragma GCC diagnostic pop
diff --git a/app/src/main/cpp/nativestreambuf.h b/app/src/main/cpp/nativestreambuf.h
index 0d288e627..3d423c503 100644
--- a/app/src/main/cpp/nativestreambuf.h
+++ b/app/src/main/cpp/nativestreambuf.h
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#ifndef FCITX5_ANDROID_NATIVESTREAMBUF_H
#define FCITX5_ANDROID_NATIVESTREAMBUF_H
diff --git a/app/src/main/cpp/object-conversion.h b/app/src/main/cpp/object-conversion.h
index a84840fde..aa4155095 100644
--- a/app/src/main/cpp/object-conversion.h
+++ b/app/src/main/cpp/object-conversion.h
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#ifndef FCITX5_ANDROID_OBJECT_CONVERSION_H
#define FCITX5_ANDROID_OBJECT_CONVERSION_H
@@ -16,6 +20,7 @@ jobject fcitxInputMethodEntryToJObject(JNIEnv *env, const fcitx::InputMethodEntr
*JString(env, entry->nativeName()),
*JString(env, entry->label()),
*JString(env, entry->languageCode()),
+ *JString(env, entry->addon()),
entry->isConfigurable()
);
}
@@ -31,16 +36,15 @@ jobjectArray fcitxInputMethodEntriesToJObjectArray(JNIEnv *env, const std::vecto
}
jobject fcitxInputMethodStatusToJObject(JNIEnv *env, const InputMethodStatus &status) {
- const auto entry = status.entry;
- if (status.subMode.empty()) return fcitxInputMethodEntryToJObject(env, entry);
return env->NewObject(GlobalRef->InputMethodEntry, GlobalRef->InputMethodEntryInitWithSubMode,
- *JString(env, entry->uniqueName()),
- *JString(env, entry->name()),
- *JString(env, entry->icon()),
- *JString(env, entry->nativeName()),
- *JString(env, entry->label()),
- *JString(env, entry->languageCode()),
- entry->isConfigurable(),
+ *JString(env, status.uniqueName),
+ *JString(env, status.name),
+ *JString(env, status.icon),
+ *JString(env, status.nativeName),
+ *JString(env, status.label),
+ *JString(env, status.languageCode),
+ *JString(env, status.addon),
+ status.configurable,
*JString(env, status.subMode),
*JString(env, status.subModeLabel),
*JString(env, status.subModeIcon)
@@ -157,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
new file mode 100644
index 000000000..d78fede55
--- /dev/null
+++ b/app/src/main/cpp/po/es.po
@@ -0,0 +1,50 @@
+#
+# Translators:
+# Adolfo Jayme-Barrientos, 2023
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: fcitx5-android\n"
+"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: Adolfo Jayme-Barrientos, 2023\n"
+"Language-Team: Spanish (https://app.transifex.com/fcitx/teams/12005/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+msgid "Android Frontend"
+msgstr "Interfaz para Android"
+
+msgid "Android Keyboard"
+msgstr "Teclado para Android"
+
+msgid "Word hint"
+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 ""
+
+msgid "Choose key modifier"
+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 73844287a..0efb63065 100644
--- a/app/src/main/cpp/po/fcitx5-android.pot
+++ b/app/src/main/cpp/po/fcitx5-android.pot
@@ -23,6 +23,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 ""
@@ -31,3 +37,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/ja.po b/app/src/main/cpp/po/ja.po
index defa4355e..d612fdf36 100644
--- a/app/src/main/cpp/po/ja.po
+++ b/app/src/main/cpp/po/ja.po
@@ -1,6 +1,7 @@
#
# Translators:
# Takuro Onoue , 2022
+# NPL, 2024
#
msgid ""
msgstr ""
@@ -8,8 +9,8 @@ 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: Takuro Onoue , 2022\n"
-"Language-Team: Japanese (https://www.transifex.com/fcitx/teams/12005/ja/)\n"
+"Last-Translator: NPL, 2024\n"
+"Language-Team: Japanese (https://app.transifex.com/fcitx/teams/12005/ja/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -21,3 +22,30 @@ msgstr "Android フロントエンド"
msgid "Android Keyboard"
msgstr "Android キーボード"
+
+msgid "Word hint"
+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 "単語ヒントのページサイズ"
+
+msgid "Choose key modifier"
+msgstr "修飾キーを選択"
+
+msgid "Insert space between words"
+msgstr "単語間に空白を入力する"
+
+msgid "Android Toast & Notification"
+msgstr "Android トーストと通知"
+
+msgid "Hidden Notifications"
+msgstr "通知を非表示にする"
diff --git a/app/src/main/cpp/po/ru.po b/app/src/main/cpp/po/ru.po
index 20040eec1..9af294da3 100644
--- a/app/src/main/cpp/po/ru.po
+++ b/app/src/main/cpp/po/ru.po
@@ -1,7 +1,7 @@
#
# Translators:
# Potato Hatsue, 2022
-# Dmitry , 2022
+# Dmitry , 2024
#
msgid ""
msgstr ""
@@ -9,8 +9,8 @@ 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: Dmitry , 2022\n"
-"Language-Team: Russian (https://www.transifex.com/fcitx/teams/12005/ru/)\n"
+"Last-Translator: Dmitry , 2024\n"
+"Language-Team: Russian (https://app.transifex.com/fcitx/teams/12005/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -29,6 +29,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 "Размер страницы подсказки слов"
@@ -37,3 +43,9 @@ msgstr "Выберите клавишу-модификатор"
msgid "Insert space between words"
msgstr "Вставить пробел между словами"
+
+msgid "Android Toast & Notification"
+msgstr "Всплывающие подсказки и уведомления Android"
+
+msgid "Hidden Notifications"
+msgstr "Скрытые уведомления"
diff --git a/app/src/main/cpp/po/zh_CN.po b/app/src/main/cpp/po/zh_CN.po
index f542a8242..5be82e30c 100644
--- a/app/src/main/cpp/po/zh_CN.po
+++ b/app/src/main/cpp/po/zh_CN.po
@@ -1,7 +1,8 @@
#
# Translators:
# Potato Hatsue, 2022
-# rocka, 2022
+# rocka, 2024
+# Yiyu Liu, 2024
#
msgid ""
msgstr ""
@@ -9,8 +10,8 @@ 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, 2022\n"
-"Language-Team: Chinese (China) (https://www.transifex.com/fcitx/teams/12005/zh_CN/)\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"
"Content-Transfer-Encoding: 8bit\n"
@@ -29,6 +30,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 "单词提示页大小"
@@ -37,3 +44,9 @@ msgstr "选词修饰键"
msgid "Insert space between words"
msgstr "在单词间插入空格"
+
+msgid "Android Toast & Notification"
+msgstr "Android 弹出提示与通知"
+
+msgid "Hidden Notifications"
+msgstr "隐藏的通知"
diff --git a/app/src/main/cpp/po/zh_TW.po b/app/src/main/cpp/po/zh_TW.po
index 3fa1be6b9..36cd22166 100644
--- a/app/src/main/cpp/po/zh_TW.po
+++ b/app/src/main/cpp/po/zh_TW.po
@@ -1,8 +1,8 @@
#
# Translators:
# 黃柏諺 , 2022
-# Zhang Jia-Bin , 2022
-# rocka, 2022
+# Jia-Bin, 2022
+# Yiyu Liu, 2024
#
msgid ""
msgstr ""
@@ -10,8 +10,8 @@ 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, 2022\n"
-"Language-Team: Chinese (Taiwan) (https://www.transifex.com/fcitx/teams/12005/zh_TW/)\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"
"Content-Transfer-Encoding: 8bit\n"
@@ -25,16 +25,28 @@ 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 "選詞修飾鍵"
msgid "Insert space between words"
msgstr "在單字間插入空格"
+
+msgid "Android Toast & Notification"
+msgstr "Android 浮動式訊息與通知"
+
+msgid "Hidden Notifications"
+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 659202e48..e52553b36 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
@@ -1,20 +1,35 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
package org.fcitx.fcitx5.android
+import android.annotation.SuppressLint
import android.app.Application
+import android.content.BroadcastReceiver
+import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
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
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
+import org.fcitx.fcitx5.android.daemon.FcitxDaemon
import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.theme.ThemeManager
import org.fcitx.fcitx5.android.ui.main.LogActivity
+import org.fcitx.fcitx5.android.utils.AppUtil
import org.fcitx.fcitx5.android.utils.Locales
import org.fcitx.fcitx5.android.utils.isDarkMode
+import org.fcitx.fcitx5.android.utils.startActivity
+import org.fcitx.fcitx5.android.utils.userManager
import timber.log.Timber
import kotlin.system.exitProcess
@@ -22,11 +37,71 @@ class FcitxApplication : Application() {
val coroutineScope = MainScope() + CoroutineName("FcitxApplication")
+ private val shutdownReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != Intent.ACTION_SHUTDOWN) return
+ Timber.d("Device shutting down, trying to save fcitx state...")
+ val fcitx = FcitxDaemon.getFirstConnectionOrNull()
+ ?: return Timber.d("No active fcitx connection, skipping")
+ fcitx.runImmediately { save() }
+ }
+ }
+
+ private val unlockReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != Intent.ACTION_USER_UNLOCKED) return
+ if (!isDirectBootMode) return
+ Timber.d("Device unlocked, app will exit now and restart to normal mode")
+ FcitxDaemon.getFirstConnectionOrNull()?.also {
+ // try to shutdown fcitx gracefully
+ FcitxDaemon.stopFcitx()
+ }
+ AppUtil.exit()
+ }
+ }
+
+ 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
+
+ val directBootAwareContext: Context
+ @SuppressLint("NewApi")
+ get() = if (isDirectBootMode) createDeviceProtectedStorageContext() else applicationContext
+
override fun onCreate() {
super.onCreate()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !userManager.isUserUnlocked) {
+ isDirectBootMode = true
+ registerReceiver(unlockReceiver, IntentFilter(Intent.ACTION_USER_UNLOCKED))
+ }
+ val ctx = directBootAwareContext
+
if (!BuildConfig.DEBUG) {
Thread.setDefaultUncaughtExceptionHandler { _, e ->
- startActivity(Intent(applicationContext, LogActivity::class.java).apply {
+ val crashTime = System.currentTimeMillis()
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx)
+ val lastCrashTimePrefKey = "last_crash_time"
+ val lastCrashTime = sharedPreferences.getLong(lastCrashTimePrefKey, -1L)
+ // make sure it was written to persistent storage
+ sharedPreferences.edit(commit = true) {
+ putLong(lastCrashTimePrefKey, crashTime)
+ }
+ if (crashTime - lastCrashTime <= 10_000L) {
+ // continuous crashes within 10 seconds, maybe in a crash loop. just bail
+ exitProcess(10)
+ }
+ startActivity {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(LogActivity.FROM_CRASH, true)
// avoid transaction overflow
@@ -37,14 +112,14 @@ class FcitxApplication : Application() {
it
}
putExtra(LogActivity.CRASH_STACK_TRACE, truncated)
- })
+ }
exitProcess(10)
}
}
instance = this
// we don't have AppPrefs available yet
- val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx)
if (BuildConfig.DEBUG || sharedPrefs.getBoolean("verbose_log", false)) {
Timber.plant(object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
@@ -60,6 +135,8 @@ class FcitxApplication : Application() {
})
}
+ Timber.d("isDirectBootMode=$isDirectBootMode")
+
AppPrefs.init(sharedPrefs)
// record last pid for crash logs
AppPrefs.getInstance().internal.pid.apply {
@@ -68,24 +145,53 @@ class FcitxApplication : Application() {
Timber.d("Last pid is $lastPid. Set it to current pid: $currentPid")
setValue(currentPid)
}
- ClipboardManager.init(applicationContext)
+ ClipboardManager.init(ctx)
ThemeManager.init(resources.configuration)
Locales.onLocaleChange(resources.configuration)
+ registerReceiver(shutdownReceiver, IntentFilter(Intent.ACTION_SHUTDOWN))
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isDirectBootMode) {
+ 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 {
private var lastPid: Int? = null
private var instance: FcitxApplication? = null
fun getInstance() =
- instance ?: throw IllegalStateException("Fcitx application is not created!")
+ instance ?: throw IllegalStateException("FcitxApplication has not been created!")
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/FcitxRemoteService.kt b/app/src/main/java/org/fcitx/fcitx5/android/FcitxRemoteService.kt
new file mode 100644
index 000000000..282a31f62
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/FcitxRemoteService.kt
@@ -0,0 +1,132 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
+package org.fcitx.fcitx5.android
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.os.Process
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.fcitx.fcitx5.android.common.ipc.IClipboardEntryTransformer
+import org.fcitx.fcitx5.android.common.ipc.IFcitxRemoteService
+import org.fcitx.fcitx5.android.core.data.DataManager
+import org.fcitx.fcitx5.android.core.reloadPinyinDict
+import org.fcitx.fcitx5.android.core.reloadQuickPhrase
+import org.fcitx.fcitx5.android.daemon.FcitxDaemon
+import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager
+import org.fcitx.fcitx5.android.utils.Const
+import org.fcitx.fcitx5.android.utils.desc
+import org.fcitx.fcitx5.android.utils.descEquals
+import timber.log.Timber
+import java.util.PriorityQueue
+
+class FcitxRemoteService : Service() {
+
+ private val clipboardTransformerLock = Mutex()
+
+ private val scope = MainScope() + CoroutineName("FcitxRemoteService")
+
+ private val clipboardTransformers =
+ PriorityQueue(3, compareByDescending { it.priority })
+
+ private fun transformClipboard(source: String): String {
+ var result = source
+ clipboardTransformers.forEach {
+ try {
+ result = it.transform(result)!!
+ } catch (e: Exception) {
+ Timber.w("Exception while calling clipboard transformer '${it.desc}'")
+ Timber.w(e)
+ }
+ }
+ return result
+ }
+
+ private suspend fun updateClipboardManager() = clipboardTransformerLock.withLock {
+ ClipboardManager.transformer =
+ if (clipboardTransformers.isEmpty()) null else ::transformClipboard
+ Timber.d("All clipboard transformers: ${clipboardTransformers.joinToString { it.desc }}")
+ }
+
+ private val binder = object : IFcitxRemoteService.Stub() {
+ override fun getVersionName(): String = Const.versionName
+
+ override fun getPid(): Int = Process.myPid()
+
+ override fun getLoadedPlugins(): MutableMap =
+ DataManager.getLoadedPlugins().map {
+ it.packageName to it.versionName
+ }.let { mutableMapOf().apply { putAll(it) } }
+
+ override fun restartFcitx() {
+ FcitxDaemon.restartFcitx()
+ }
+
+ override fun registerClipboardEntryTransformer(transformer: IClipboardEntryTransformer) {
+ Timber.d("registerClipboardEntryTransformer: ${transformer.desc}")
+ if (transformer.description.isNullOrBlank()) {
+ Timber.w("Cannot register ClipboardEntryTransformer of null or empty description")
+ }
+ if (clipboardTransformers.any { it.descEquals(transformer) }) {
+ Timber.w("ClipboardEntryTransformer ${transformer.desc} has already been registered")
+ return
+ }
+ scope.launch {
+ transformer.asBinder().linkToDeath({
+ unregisterClipboardEntryTransformer(transformer)
+ }, 0)
+ clipboardTransformers.add(transformer)
+ updateClipboardManager()
+ }
+ }
+
+ override fun unregisterClipboardEntryTransformer(transformer: IClipboardEntryTransformer) {
+ Timber.d("unregisterClipboardEntryTransformer: ${transformer.desc}")
+ scope.launch {
+ clipboardTransformers.remove(transformer)
+ || clipboardTransformers.removeAll { it.descEquals(transformer) }
+ || return@launch
+ updateClipboardManager()
+ }
+ }
+
+ override fun reloadPinyinDict() {
+ FcitxDaemon.getFirstConnectionOrNull()?.runIfReady { reloadPinyinDict() }
+ }
+
+ override fun reloadQuickPhrase() {
+ FcitxDaemon.getFirstConnectionOrNull()?.runIfReady { reloadQuickPhrase() }
+ }
+ }
+
+ override fun onCreate() {
+ Timber.d("FcitxRemoteService onCreate")
+ super.onCreate()
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ Timber.d("FcitxRemoteService onBind: $intent")
+ return binder
+ }
+
+ override fun onUnbind(intent: Intent): Boolean {
+ Timber.d("FcitxRemoteService onUnbind: $intent")
+ return super.onUnbind(intent)
+ }
+
+ override fun onDestroy() {
+ Timber.d("FcitxRemoteService onDestroy")
+ scope.cancel()
+ clipboardTransformers.clear()
+ runBlocking { updateClipboardManager() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/AddonSubconfig.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/AddonSubconfig.kt
index 36e7b23b1..0f56aa830 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/AddonSubconfig.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/AddonSubconfig.kt
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
package org.fcitx.fcitx5.android.core
suspend fun FcitxAPI.reloadPinyinDict() = setAddonSubConfig("pinyin", "dictmanager")
@@ -8,4 +12,6 @@ suspend fun FcitxAPI.getPunctuationConfig(lang: String) =
suspend fun FcitxAPI.savePunctuationConfig(lang: String = "zh_CN", config: RawConfig) =
setAddonSubConfig("punctuation", "punctuationmap/$lang", config)
-suspend fun FcitxAPI.reloadQuickPhrase() = setAddonSubConfig("quickphrase", "editor")
\ No newline at end of file
+suspend fun FcitxAPI.reloadQuickPhrase() = setAddonSubConfig("quickphrase", "editor")
+
+suspend fun FcitxAPI.reloadPinyinCustomPhrase() = setAddonSubConfig("pinyin", "customphrase")
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 6f58477a9..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
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
package org.fcitx.fcitx5.android.core
import android.text.InputType
@@ -6,7 +10,7 @@ import splitties.bitflags.hasFlag
/**
* translated from
- * [fcitx-utils/capabilityflags.h](https://github.com/fcitx/fcitx5/blob/5.0.13/src/lib/fcitx-utils/capabilityflags.h)
+ * [fcitx-utils/capabilityflags.h](https://github.com/fcitx/fcitx5/blob/5.1.1/src/lib/fcitx-utils/capabilityflags.h)
*/
@Suppress("unused")
enum class CapabilityFlag(val flag: ULong) {
@@ -60,6 +64,17 @@ enum class CapabilityFlag(val flag: ULong) {
*/
ClientSideInputPanel(1UL shl 39),
+ /**
+ * Whether client request input method to be disabled.
+ * Usually this means only allow to type with raw keyboard.
+ */
+ Disable(1UL shl 40),
+
+ /**
+ * Whether client support commit string with cursor location.
+ */
+ CommitStringWithCursor(1UL shl 41),
+
PasswordOrSensitive(Password.flag or Sensitive.flag);
}
@@ -75,7 +90,8 @@ value class CapabilityFlags constructor(val flags: ULong) {
val DefaultFlags = CapabilityFlags(
CapabilityFlag.Preedit,
- CapabilityFlag.ClientUnfocusCommit
+ CapabilityFlag.ClientUnfocusCommit,
+ CapabilityFlag.CommitStringWithCursor
)
fun fromEditorInfo(info: EditorInfo): CapabilityFlags {
@@ -132,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 355ed5716..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,19 +1,28 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ */
package org.fcitx.fcitx5.android.core
import android.content.Context
+import android.os.Build
import androidx.annotation.Keep
+import androidx.core.content.ContextCompat
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
+import org.fcitx.fcitx5.android.FcitxApplication
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.data.DataManager
import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.utils.ImmutableGraph
import org.fcitx.fcitx5.android.utils.Locales
+import org.fcitx.fcitx5.android.utils.appContext
+import org.fcitx.fcitx5.android.utils.toast
import timber.log.Timber
/**
@@ -64,17 +73,29 @@ 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 isEmpty(): Boolean = withFcitxContext { isInputPanelEmpty() }
@@ -134,8 +155,8 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun triggerQuickPhrase() = withFcitxContext { triggerQuickPhraseInput() }
override suspend fun triggerUnicode() = withFcitxContext { triggerUnicodeInput() }
- private suspend fun setClipboard(string: String) =
- withFcitxContext { setFcitxClipboard(string) }
+ private suspend fun setClipboard(string: String, password: Boolean = false) =
+ withFcitxContext { setFcitxClipboard(string, password) }
override suspend fun focus(focus: Boolean) = withFcitxContext { focusInputContext(focus) }
override suspend fun activate(uid: Int, pkgName: String) =
@@ -154,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