diff --git a/.github/workflows/fdroid.yml b/.github/workflows/fdroid.yml
index 8d612b32e..3464be1ce 100644
--- a/.github/workflows/fdroid.yml
+++ b/.github/workflows/fdroid.yml
@@ -32,12 +32,12 @@ jobs:
fail-fast: false
steps:
- name: Fetch fdroiddata
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
repository: f-droid/fdroiddata
- name: Fetch fdroidserver
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
repository: f-droid/fdroidserver
path: fdroidserver
@@ -117,7 +117,7 @@ jobs:
$fdroid build --verbose --test --scan-binary --on-server --no-tarball $build
- name: Upload Artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
if: ${{ success() || failure() }}
with:
name: fdroid-${{ matrix.abi }}
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 01968ac76..f13ce1861 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch source code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 43438a178..2e2debb0f 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch source code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -24,7 +24,7 @@ jobs:
sudo apt install extra-cmake-modules gettext
- name: Setup Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "17"
@@ -35,7 +35,7 @@ jobs:
packages: cmake;3.31.6
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Publish build convention and libs
env:
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index dcc86c2e1..8618b3824 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -21,13 +21,13 @@ jobs:
- windows-2022
steps:
- name: Fetch source code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Setup Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "17"
@@ -58,7 +58,7 @@ jobs:
Add-Content $env:GITHUB_PATH "C:/msys64/ucrt64/bin"
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
- name: Build Release APK
run: |
@@ -66,7 +66,7 @@ jobs:
./gradlew :assembleReleasePlugins
- name: Upload app
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: app-${{ matrix.os }}
path: app/build/outputs/apk/release/
@@ -84,7 +84,7 @@ jobs:
done
- name: Upload plugins
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: plugins-${{ matrix.os }}
path: plugins-to-upload
diff --git a/README.md b/README.md
index 85f9bba0f..842a2332f 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,30 @@
## Download
-### Latest CI builds
-
-Jenkins: [](https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/)
-
-### Tagged releases
-
-GitHub: [](https://github.com/fcitx5-android/fcitx5-android/releases)
-
+[
](https://github.com/fcitx5-android/fcitx5-android/releases/latest)
[
](https://f-droid.org/packages/org.fcitx.fcitx5.android)
[
](https://play.google.com/store/apps/details?id=org.fcitx.fcitx5.android)
+You can also download the **latest CI build** on our Jeninks server: [](https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/)
+
+> [!NOTE]
+> APKs downloaded from GitHub Release/F-Droid/Jenkins have the same signature, which means they're compatible when upgrading, but Google Play's do not.
+>
+> (click here for detailed signature info)
+>
+> - Package Name:
org.fcitx.fcitx5.android
+> - Certificate SHA-256 fingerprint:
+>
+> - GitHub Release/Jenkins/F-Droid
+> E4:DB:1E:9E:DF:F1:36:29:D0:7D:E4:BB:F8:16:5F:E9:BD:85:57:AB:55:09:26:72:DA:8E:40:DB:E4:84:EC:D7
+> - Google Play
+> 06:53:6F:F6:E8:76:C0:14:E1:4B:44:6F:61:FA:2B:80:9E:06:67:39:A1:D1:17:0D:0A:7A:89:88:4C:48:00:33
+>
+>
+>
+
+In case you want Fcitx5 on other platforms: [macOS](https://github.com/fcitx-contrib/fcitx5-macos), [iOS](https://github.com/fcitx-contrib/fcitx5-ios), [HarmonyOS](https://github.com/fcitx-contrib/fcitx5-harmony), [ChromeOS](https://github.com/fcitx-contrib/fcitx5-chrome), [Windows](https://github.com/fcitx-contrib/fcitx5-windows); or [try Fcitx5 in the browser](https://fcitx-contrib.github.io/online/index.html)
+
## Project status
### Supported Languages
@@ -41,6 +54,7 @@ GitHub: [
EventType::InputContextInputMethodActivated,
EventWatcherPhase::Default,
[this](Event &event) {
- FCITX_UNUSED(event);
- imChangeCallback();
+ auto &e = static_cast(event);
+ if (e.inputContext() != activeIC_) return;
+ imChangeCallback(makeInputMethodStatus(activeIC_));
+ }
+ ));
+ eventHandlers_.emplace_back(instance_->watchEvent(
+ EventType::InputContextSwitchInputMethod,
+ EventWatcherPhase::Default,
+ [this](Event &event) {
+ auto &e = static_cast(event);
+ if (e.inputContext() != activeIC_) return;
+ const auto &reason = static_cast::type>(e.reason());
+ const std::string &oldIM = e.oldInputMethod();
+ switchInputMethodCallback(reason, oldIM);
}
));
eventHandlers_.emplace_back(instance_->watchEvent(
@@ -295,20 +309,19 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
EventWatcherPhase::Default,
[this](Event &event) {
auto &e = static_cast(event);
+ if (e.inputContext() != activeIC_) return;
switch (e.component()) {
case UserInterfaceComponent::InputPanel: {
- if (activeIC_) {
- activeIC_->updateInputPanel();
- if (pagingMode_ == 0) {
- activeIC_->updateCandidatesBulk();
- } else {
- activeIC_->updateCandidatesPaged();
- }
+ activeIC_->updateInputPanel();
+ if (pagingMode_ == 0) {
+ activeIC_->updateCandidatesBulk();
+ } else {
+ activeIC_->updateCandidatesPaged();
}
break;
}
case UserInterfaceComponent::StatusArea: {
- statusAreaUpdateCallback();
+ statusAreaUpdateCallback(makeStatusAreaActions(activeIC_), makeInputMethodStatus(activeIC_));
break;
}
}
@@ -442,6 +455,11 @@ void AndroidFrontend::showToast(const std::string &s) {
void AndroidFrontend::setCandidatePagingMode(const int mode) {
pagingMode_ = mode;
+ if (mode == 0) {
+ activeIC_->updateCandidatesBulk();
+ } else {
+ activeIC_->updateCandidatesPaged();
+ }
}
void AndroidFrontend::updatePagedCandidate(const PagedCandidateEntity &paged) {
@@ -489,6 +507,28 @@ void AndroidFrontend::setPagedCandidateCallback(const PagedCandidateCallback &ca
pagedCandidateCallback = callback;
}
+void AndroidFrontend::setSwitchInputMethodCallback(const SwitchInputMethodCallback &callback) {
+ switchInputMethodCallback = callback;
+}
+
+InputMethodStatus AndroidFrontend::makeInputMethodStatus(InputContext *ic) {
+ auto *entry = instance_->inputMethodEntry(ic);
+ auto *engine = instance_->inputMethodEngine(ic);
+ return {entry, engine, ic};
+}
+
+std::vector AndroidFrontend::makeStatusAreaActions(fcitx::InputContext *ic) {
+ auto actions = std::vector();
+ for (auto group: {fcitx::StatusGroup::BeforeInputMethod,
+ fcitx::StatusGroup::InputMethod,
+ fcitx::StatusGroup::AfterInputMethod}) {
+ for (auto act: ic->statusArea().actions(group)) {
+ actions.emplace_back(act, ic);
+ }
+ }
+ return actions;
+}
+
class AndroidFrontendFactory : public AddonFactory {
public:
AddonInstance *create(AddonManager *manager) override {
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.h b/app/src/main/cpp/androidfrontend/androidfrontend.h
index 9b9db800b..cf1cbe191 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.h
@@ -23,30 +23,30 @@ class AndroidFrontend : public AddonInstance {
Instance *instance() { return instance_; }
- void updateCandidateList(const std::vector &candidates, const int size);
- void commitString(const std::string &str, const int cursor);
+ void updateCandidateList(const std::vector &candidates, int size);
+ void commitString(const std::string &str, int cursor);
void updateClientPreedit(const Text &clientPreedit);
void updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown);
- void releaseInputContext(const int uid);
+ void releaseInputContext(int uid);
void updatePagedCandidate(const PagedCandidateEntity &paged);
- void keyEvent(const Key &key, bool isRelease, const int timestamp);
+ void keyEvent(const Key &key, bool isRelease, int timestamp);
void forwardKey(const Key &key, bool isRelease);
bool selectCandidate(int idx);
bool isInputPanelEmpty();
void resetInputContext();
void repositionCursor(int idx);
void focusInputContext(bool focus);
- void activateInputContext(const int uid, const std::string &pkgName);
- void deactivateInputContext(const int uid);
+ void activateInputContext(int uid, const std::string &pkgName);
+ void deactivateInputContext(int uid);
[[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);
+ std::vector getCandidates(int offset, int limit);
+ std::vector getCandidateActions(int idx);
+ void triggerCandidateAction(int idx, int actionIdx);
+ void deleteSurrounding(int before, int after);
void showToast(const std::string &s);
- void setCandidatePagingMode(const int mode);
+ void setCandidatePagingMode(int mode);
void offsetCandidatePage(int delta);
void setCandidateListCallback(const CandidateListCallback &callback);
void setCommitStringCallback(const CommitStringCallback &callback);
@@ -58,6 +58,7 @@ class AndroidFrontend : public AddonInstance {
void setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback);
void setToastCallback(const ToastCallback &callback);
void setPagedCandidateCallback(const PagedCandidateCallback &callback);
+ void setSwitchInputMethodCallback(const SwitchInputMethodCallback &callback);
private:
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, keyEvent);
@@ -86,6 +87,7 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setToastCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPagedCandidateCallback);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setSwitchInputMethodCallback);
Instance *instance_;
FocusGroup focusGroup_;
@@ -99,11 +101,15 @@ class AndroidFrontend : public AddonInstance {
ClientPreeditCallback preeditCallback = [](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 = [] {};
+ InputMethodChangeCallback imChangeCallback = [](const InputMethodStatus &) {};
+ StatusAreaUpdateCallback statusAreaUpdateCallback = [](const std::vector &, const InputMethodStatus &) {};
DeleteSurroundingCallback deleteSurroundingCallback = [](const int, const int) {};
ToastCallback toastCallback = [](const std::string &) {};
PagedCandidateCallback pagedCandidateCallback = [](const PagedCandidateEntity &) {};
+ SwitchInputMethodCallback switchInputMethodCallback = [](const int, const std::string &) {};
+
+ InputMethodStatus makeInputMethodStatus(InputContext* ic);
+ std::vector makeStatusAreaActions(InputContext* ic);
};
} // namespace fcitx
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend_public.h b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
index 69bcbbdd5..86d83c966 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend_public.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
@@ -17,11 +17,12 @@ 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 InputMethodChangeCallback;
+typedef std::function &, const InputMethodStatus &)> StatusAreaUpdateCallback;
typedef std::function DeleteSurroundingCallback;
typedef std::function ToastCallback;
typedef std::function PagedCandidateCallback;
+typedef std::function SwitchInputMethodCallback;
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, keyEvent,
void(const fcitx::Key &, bool isRelease, const int timestamp))
@@ -101,4 +102,7 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setToastCallback,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setPagedCandidateCallback,
void(const PagedCandidateCallback &))
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setSwitchInputMethodCallback,
+ void(const SwitchInputMethodCallback &))
+
#endif // FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
index 6ab758b02..a875f796b 100644
--- a/app/src/main/cpp/native-lib.cpp
+++ b/app/src/main/cpp/native-lib.cpp
@@ -154,7 +154,7 @@ class Fcitx {
auto *ic = p_frontend->call();
if (!ic) return nullptr;
auto *entry = p_instance->inputMethodEntry(ic);
- auto *engine = static_cast(p_instance->addonManager().addon(entry->addon(), true));
+ auto *engine = p_instance->inputMethodEngine(ic);
return std::make_unique(entry, engine, ic);
}
@@ -637,21 +637,16 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(vararg, 4, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, timestamp));
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 5, *vararg);
};
- auto imChangeCallback = []() {
- std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
- if (!status) return;
+ auto imChangeCallback = [](const InputMethodStatus &status) {
auto env = GlobalRef->AttachEnv();
- auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
- auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
+ auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->Object, nullptr));
+ auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, status));
env->SetObjectArrayElement(vararg, 0, obj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 6, *vararg);
};
- auto statusAreaUpdateCallback = []() {
- std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
- if (!status) return;
+ auto statusAreaUpdateCallback = [](const std::vector &actions, const InputMethodStatus &status) {
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(2, GlobalRef->Object, nullptr));
auto actionArray = JRef(env, env->NewObjectArray(static_cast(actions.size()), GlobalRef->Action, nullptr));
int i = 0;
for (const auto &a: actions) {
@@ -659,7 +654,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(actionArray, i++, obj);
}
env->SetObjectArrayElement(vararg, 0, actionArray);
- auto statusObj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
+ auto statusObj = JRef(env, fcitxInputMethodStatusToJObject(env, status));
env->SetObjectArrayElement(vararg, 1, statusObj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 7, *vararg);
};
@@ -696,6 +691,14 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(vararg, 4, hasNext);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 9, *vararg);
};
+ auto switchInputMethodCallback = [](const int reason, const std::string &oldIM) {
+ auto env = GlobalRef->AttachEnv();
+ auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->Object, nullptr));
+ auto reasonInteger = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, reason));
+ env->SetObjectArrayElement(vararg, 0, reasonInteger);
+ env->SetObjectArrayElement(vararg, 1, JString(env, oldIM));
+ env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 10, *vararg);
+ };
auto toastCallback = [](const std::string &s) {
auto env = GlobalRef->AttachEnv();
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->ShowToast, *JString(env, s));
@@ -716,6 +719,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
androidfrontend->template call(statusAreaUpdateCallback);
androidfrontend->template call(deleteSurroundingCallback);
androidfrontend->template call(pagedCandidateCallback);
+ androidfrontend->template call(switchInputMethodCallback);
androidfrontend->template call(toastCallback);
});
FCITX_INFO() << "Finishing startup";
@@ -992,6 +996,15 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_focusInputContext(JNIEnv *env, jclass c
Fcitx::Instance().focusInputContext(focus);
}
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_focusInputContextOutIn(JNIEnv *env, jclass clazz) {
+ RETURN_IF_NOT_RUNNING
+ auto &instance = Fcitx::Instance();
+ instance.focusInputContext(false);
+ instance.focusInputContext(true);
+}
+
extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_activateInputContext(JNIEnv *env, jclass clazz, jint uid, jstring pkg_name) {
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 c70339043..1b8dfa5d5 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
@@ -165,6 +165,7 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
withFcitxContext { setFcitxClipboard(string, password) }
override suspend fun focus(focus: Boolean) = withFcitxContext { focusInputContext(focus) }
+ override suspend fun focusOutIn() = withFcitxContext { focusInputContextOutIn() }
override suspend fun activate(uid: Int, pkgName: String) =
withFcitxContext { activateInputContext(uid, pkgName) }
@@ -344,6 +345,9 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
@JvmStatic
external fun focusInputContext(focus: Boolean)
+ @JvmStatic
+ external fun focusInputContextOutIn()
+
@JvmStatic
external fun activateInputContext(uid: Int, pkgName: String)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
index b17083e5f..3d2af7db4 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
@@ -91,6 +91,7 @@ interface FcitxAPI {
suspend fun triggerUnicode()
suspend fun focus(focus: Boolean = true)
+ suspend fun focusOutIn()
suspend fun activate(uid: Int, pkgName: String)
suspend fun deactivate(uid: Int)
suspend fun setCapFlags(flags: CapabilityFlags)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
index 1c119fab2..55e3d8c04 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
@@ -133,8 +133,8 @@ sealed class FcitxEvent(open val data: T) {
override val eventType = EventType.PagedCandidate
- enum class LayoutHint(value: Int) {
- NotSet(0), Vertical(1), Horizontal(2);
+ enum class LayoutHint {
+ NotSet, Vertical, Horizontal;
companion object {
private val Types = entries.toTypedArray()
@@ -180,6 +180,30 @@ sealed class FcitxEvent(open val data: T) {
}
}
+ data class SwitchInputMethodEvent(override val data: Data) :
+ FcitxEvent(data) {
+
+ override val eventType: EventType
+ get() = EventType.SwitchInputMethod
+
+ /**
+ * The reason why input method is switched to another.
+ *
+ * translated from
+ * [fcitx/event.h](https://github.com/fcitx/fcitx5/blob/5.1.16/src/lib/fcitx/event.h#L41)
+ */
+ enum class Reason {
+ Trigger, Deactivate, AltTrigger, Activate, Enumerate, GroupChange, CapabilityChanged, Other;
+
+ companion object {
+ private val Types = entries.toTypedArray()
+ fun of(value: Int) = Types[value]
+ }
+ }
+
+ data class Data(val reason: Reason, val oldInputMethod: String)
+ }
+
data class UnknownEvent(override val data: Array) : FcitxEvent>(data) {
override val eventType = EventType.Unknown
@@ -211,6 +235,7 @@ sealed class FcitxEvent(open val data: T) {
StatusArea,
DeleteSurrounding,
PagedCandidate,
+ SwitchInputMethod,
Unknown
}
@@ -274,6 +299,12 @@ sealed class FcitxEvent(open val data: T) {
)
)
}
+ EventType.SwitchInputMethod -> SwitchInputMethodEvent(
+ SwitchInputMethodEvent.Data(
+ SwitchInputMethodEvent.Reason.of(params[0] as Int),
+ params[1] as String
+ )
+ )
else -> UnknownEvent(params)
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Key.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Key.kt
index d72b78ebe..e9e4e451d 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/Key.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Key.kt
@@ -24,7 +24,7 @@ data class Key(
override fun toString() = portableString
companion object {
- val None = Key( 0, 0, "", "")
+ val None = Key(0, 0, "", "")
@JvmStatic
external fun parse(raw: String): Key
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
index 19ab51286..5bcde21a6 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
@@ -97,6 +97,18 @@ value class KeyStates(val states: UInt) {
fun of(v: Int) = KeyStates(v.toUInt())
fun fromKeyEvent(event: KeyEvent): KeyStates {
+ // drop modifier state when using combination keys to input number/symbol on some phones
+ // because fcitx doesn't recognize selection key with modifiers (eg. Alt+Q for 1)
+ // in which case event.getNumber().toInt() == event.getUnicodeChar()
+ // ... but some keys can have event.getNumber() return 0, need to check displayLabel as well
+ val unicode = event.unicodeChar
+ // skip ' ', because it would produce same unicodeChar regardless of the modifier
+ if (unicode != 0 && unicode != ' '.code) {
+ val char = unicode.toChar()
+ if (char == event.number || event.displayLabel.uppercaseChar() != char.uppercaseChar()) {
+ return Empty
+ }
+ }
var states = KeyState.NoState.state
event.apply {
if (isAltPressed) states += KeyState.Alt
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/KeySym.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/KeySym.kt
index 6bc2c3531..40eb1d77c 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/KeySym.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/KeySym.kt
@@ -1,9 +1,10 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
+import android.view.KeyCharacterMap
import android.view.KeyEvent
@JvmInline
@@ -14,7 +15,22 @@ value class KeySym(val sym: Int) {
override fun toString() = "0x" + sym.toString(16).padStart(4, '0')
companion object {
- fun fromKeyEvent(event: KeyEvent) =
- FcitxKeyMapping.keyCodeToSym(event.keyCode)?.let { KeySym(it) }
+ fun fromKeyEvent(event: KeyEvent): KeySym? {
+ val charCode = event.unicodeChar
+ // try charCode first, allow upper and lower case characters generating different KeySym
+ if (charCode != 0 &&
+ // skip \t, because it's charCode is different from KeySym
+ charCode != '\t'.code &&
+ // skip \n, because fcitx wants \r for return
+ charCode != '\n'.code &&
+ // skip Android's private-use character
+ charCode != KeyCharacterMap.HEX_INPUT.code &&
+ charCode != KeyCharacterMap.PICKER_DIALOG_INPUT.code
+ ) {
+ return KeySym(charCode)
+ }
+ return KeySym(FcitxKeyMapping.keyCodeToSym(event.keyCode) ?: return null)
+ }
+
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
index 4de1d5817..9fdafdcfe 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
@@ -6,6 +6,7 @@
package org.fcitx.fcitx5.android.data
import android.content.Context
+import android.os.Build
import androidx.core.content.edit
import kotlinx.serialization.json.Json
import org.fcitx.fcitx5.android.FcitxApplication
@@ -50,7 +51,15 @@ class RecentlyUsed(val type: String, val limit: Int) {
}
fun insert(item: String) {
- map.put(item, true)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ map.putLast(item, true)
+ } else {
+ // LinkedHashMap's encounter order is not affected when `put` an existing key
+ if (map.containsKey(item)) {
+ map.remove(item)
+ }
+ map.put(item, true)
+ }
save()
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt
index a42ef8d52..d3b4948dd 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/theme/ThemePrefs.kt
@@ -36,6 +36,11 @@ class ThemePrefs(sharedPreferences: SharedPreferences) :
val keyBorder = switch(R.string.key_border, "key_border", false)
+ val keyBorderStroke = switch(
+ R.string.key_border_stroke, "key_border_stroke", false,
+ enableUiOn = { keyBorder.getValue() }
+ )
+
val keyRippleEffect = switch(R.string.key_ripple_effect, "key_ripple_effect", false)
val keyHorizontalMargin: ManagedPreference.PInt
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt
index c43731d39..a3dc2c707 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/BaseInputView.kt
@@ -37,6 +37,19 @@ abstract class BaseInputView(
}
}
+ open fun refreshWithCachedEvents() {
+ val inputPanelData = fcitx.runImmediately { inputPanelCached }
+ val inputMethodEntry = fcitx.runImmediately { inputMethodEntryCached }
+ val statusAreaActions = fcitx.runImmediately { statusAreaActionsCached }
+ arrayOf(
+ FcitxEvent.InputPanelEvent(inputPanelData),
+ FcitxEvent.IMChangeEvent(inputMethodEntry),
+ FcitxEvent.StatusAreaEvent(
+ FcitxEvent.StatusAreaEvent.Data(statusAreaActions, inputMethodEntry)
+ )
+ ).forEach { handleFcitxEvent(it) }
+ }
+
var handleEvents = false
set(value) {
field = value
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt
index bf9ad00d9..39b127278 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt
@@ -69,6 +69,7 @@ import org.fcitx.fcitx5.android.utils.InputMethodUtil
import org.fcitx.fcitx5.android.utils.alpha
import org.fcitx.fcitx5.android.utils.forceShowSelf
import org.fcitx.fcitx5.android.utils.inputMethodManager
+import org.fcitx.fcitx5.android.utils.isTypeNull
import org.fcitx.fcitx5.android.utils.monitorCursorAnchor
import org.fcitx.fcitx5.android.utils.styledFloat
import org.fcitx.fcitx5.android.utils.withBatchEdit
@@ -87,6 +88,12 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
private val cachedKeyEvents = LruCache(78)
private var cachedKeyEventIndex = 0
+ /**
+ * Saves MetaState produced by hardware keyboard with "sticky" modifier keys, to clear them in order.
+ * See also [InputConnection#clearMetaKeyStates(int)](https://developer.android.com/reference/android/view/inputmethod/InputConnection#clearMetaKeyStates(int))
+ */
+ private var lastMetaState: Int = 0
+
private lateinit var pkgNameCache: PackageNameCache
private lateinit var decorView: View
@@ -244,12 +251,40 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
// KeyEvent from physical keyboard (or input method engine forwardKey)
// use cached event if available
cachedKeyEvents.remove(it.timestamp)?.let { keyEvent ->
+ /**
+ * intercept the KeyEvent which would cause the default [android.text.method.QwertyKeyListener]
+ * to show a Gingerbread-style CharacterPickerDialog
+ */
+ if (keyEvent.unicodeChar == KeyCharacterMap.PICKER_DIALOG_INPUT.code) {
+ currentInputConnection?.sendKeyEvent(
+ KeyEvent(
+ keyEvent.downTime, keyEvent.eventTime,
+ keyEvent.action, keyEvent.keyCode,
+ keyEvent.repeatCount, keyEvent.metaState, -1,
+ keyEvent.scanCode, keyEvent.flags, keyEvent.source
+ )
+ )
+ return@event
+ }
// If you also want cached Enter key presses to go through your custom logic:
- if (keyEvent.keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) {
- handleReturnKey()
- return@event // Skip sending the raw keyEvent if handled
- }
+ if (keyEvent.keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) {
+ handleReturnKey()
+ return@event // Skip sending the raw keyEvent if handled
+ }
currentInputConnection?.sendKeyEvent(keyEvent)
+ if (KeyEvent.isModifierKey(keyEvent.keyCode)) {
+ when (keyEvent.action) {
+ KeyEvent.ACTION_DOWN -> {
+ // save current metaState when modifier key down
+ lastMetaState = keyEvent.metaState
+ }
+ KeyEvent.ACTION_UP -> {
+ // only clear metaState that would be missing when this modifier key up
+ currentInputConnection?.clearMetaKeyStates(lastMetaState xor keyEvent.metaState)
+ lastMetaState = keyEvent.metaState
+ }
+ }
+ }
return@event
}
// simulate key event
@@ -288,6 +323,17 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
switchInputMethod(InputMethodUtil.componentName, subtype)
}
}
+ is FcitxEvent.SwitchInputMethodEvent -> {
+ val (reason) = event.data
+ if (reason != FcitxEvent.SwitchInputMethodEvent.Reason.CapabilityChanged &&
+ reason != FcitxEvent.SwitchInputMethodEvent.Reason.Other
+ ) {
+ if (inputDeviceMgr.evaluateOnInputMethodChange()) {
+ // show inputView for [CandidatesView] when input method switched by user
+ forceShowSelf()
+ }
+ }
+ }
else -> {}
}
}
@@ -548,27 +594,12 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
// KeyUp and KeyDown events actually can happen on the same time
val timestamp = cachedKeyEventIndex++
cachedKeyEvents.put(timestamp, event)
- val up = event.action == KeyEvent.ACTION_UP
- val states = KeyStates.fromKeyEvent(event)
- val charCode = event.unicodeChar
- // try send charCode first, allow upper case and lower case character generating different KeySym
- // skip \t, because it's charCode is different from KeySym
- // skip \n, because fcitx wants \r for return
- // skip ' ', because it would produce same KeySym regardless of the modifier
- if (charCode > 0 && charCode != '\t'.code && charCode != '\n'.code && charCode != ' '.code) {
- // drop modifier state when using combination keys to input number/symbol on some phones
- // because fcitx doesn't recognize selection key with modifiers (eg. Alt+Q for 1)
- // in which case event.getNumber().toInt() == event.getUnicodeChar()
- val s = if (event.number.code == charCode) KeyStates.Empty else states
+ val sym = KeySym.fromKeyEvent(event)
+ if (sym != null) {
+ val states = KeyStates.fromKeyEvent(event)
+ val up = event.action == KeyEvent.ACTION_UP
postFcitxJob {
- sendKey(charCode, s.states, event.scanCode, up, timestamp)
- }
- return true
- }
- val keySym = KeySym.fromKeyEvent(event)
- if (keySym != null) {
- postFcitxJob {
- sendKey(keySym, states, event.scanCode, up, timestamp)
+ sendKey(sym, states, event.scanCode, up, timestamp)
}
return true
}
@@ -668,7 +699,10 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
resetComposingState()
val flags = CapabilityFlags.fromEditorInfo(attribute)
capabilityFlags = flags
+ // EditorInfo may change between onStartInput and onStartInputView
+ inputDeviceMgr.notifyOnStartInput(attribute)
Timber.d("onStartInput: initialSel=${selection.current}, restarting=$restarting")
+ val isNullType = attribute.isTypeNull()
// wait until InputContext created/activated
postFcitxJob {
if (restarting) {
@@ -680,6 +714,10 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
// EditorInfo can be different in onStartInput and onStartInputView,
// especially in browsers
setCapFlags(flags)
+ // for hardware keyboard, focus to allow switching input methods before onStartInputView
+ if (!isNullType) {
+ focus(true)
+ }
}
}
@@ -827,8 +865,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
// `fcitx.reset()` here would commit preedit after new cursor position
// since we have `ClientUnfocusCommit`, focus out and in would do the trick
postFcitxJob {
- focus(false)
- focus(true)
+ focusOutIn()
}
}
}
@@ -976,13 +1013,16 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
resetComposingState()
postFcitxJob {
- focus(false)
+ focusOutIn()
}
showingDialog?.dismiss()
}
override fun onFinishInput() {
Timber.d("onFinishInput")
+ postFcitxJob {
+ focus(false)
+ }
capabilityFlags = CapabilityFlags.DefaultFlags
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt
index 656f40dfd..44ac28f96 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputDeviceManager.kt
@@ -5,13 +5,13 @@
package org.fcitx.fcitx5.android.input
-import android.text.InputType
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesMode
+import org.fcitx.fcitx5.android.utils.isTypeNull
import org.fcitx.fcitx5.android.utils.monitorCursorAnchor
class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
@@ -20,16 +20,25 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
private var candidatesView: CandidatesView? = null
private fun setupInputViewEvents(isVirtual: Boolean) {
- inputView?.handleEvents = isVirtual
- inputView?.visibility = if (isVirtual) View.VISIBLE else View.GONE
+ val iv = inputView ?: return
+ iv.handleEvents = isVirtual
+ if (isVirtual) {
+ iv.visibility = View.VISIBLE
+ iv.refreshWithCachedEvents()
+ } else {
+ iv.visibility = View.GONE
+ }
}
private fun setupCandidatesViewEvents(isVirtual: Boolean) {
- candidatesView?.handleEvents = !isVirtual
+ val cv = candidatesView ?: return
+ cv.handleEvents = !isVirtual
// hide CandidatesView when entering virtual keyboard mode,
// but preserve the visibility when entering physical keyboard mode (in case it's empty)
if (isVirtual) {
- candidatesView?.visibility = View.GONE
+ cv.visibility = View.GONE
+ } else {
+ cv.refreshWithCachedEvents()
}
}
@@ -72,12 +81,16 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
private var candidatesViewMode by AppPrefs.getInstance().candidates.mode
+ fun notifyOnStartInput(attribute: EditorInfo) {
+ isNullInputType = attribute.isTypeNull()
+ }
+
/**
* @return should use virtual keyboard
*/
fun evaluateOnStartInputView(info: EditorInfo, service: FcitxInputMethodService): Boolean {
startedInputView = true
- isNullInputType = info.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL
+ isNullInputType = info.isTypeNull()
val useVirtualKeyboard = when (candidatesViewMode) {
FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
FloatingCandidatesMode.InputDevice -> isVirtualKeyboard
@@ -88,12 +101,12 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
}
/**
- * @return should force show input views
+ * @return should force show input views on hardware key down
*/
fun evaluateOnKeyDown(e: KeyEvent, service: FcitxInputMethodService): Boolean {
if (startedInputView) {
// filter out back/home/volume buttons and combination keys
- if (e.isPrintingKey && e.hasNoModifiers()) {
+ if (e.unicodeChar != 0) {
// evaluate virtual keyboard visibility when pressing physical keyboard while InputView visible
evaluateOnKeyDownInner(service)
}
@@ -102,7 +115,7 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
} else {
// force show InputView when focusing on text input (likely inputType is not TYPE_NULL)
// and pressing any digit/letter/punctuation key on physical keyboard
- val showInputView = !isNullInputType && e.isPrintingKey && e.hasNoModifiers()
+ val showInputView = !isNullInputType && e.unicodeChar != 0
if (showInputView) {
evaluateOnKeyDownInner(service)
}
@@ -140,6 +153,13 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
applyMode(service, useVirtualKeyboard)
}
+ /**
+ * @return should force show inputView for [CandidatesView] when input method switched by user
+ */
+ fun evaluateOnInputMethodChange(): Boolean {
+ return !isVirtualKeyboard && !startedInputView
+ }
+
fun onFinishInputView() {
startedInputView = false
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt
index 8b87ab58c..eeb5f77a3 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.input.candidates.expanded.window
@@ -143,11 +143,16 @@ abstract class BaseExpandedCandidateWindow> :
fcitx.launchOnReady { it.select(holder.idx) }
}
holder.itemView.setOnLongClickListener {
- horizontalCandidate.showCandidateActionMenu(holder.idx, holder.text, holder.ui)
+ horizontalCandidate.showCandidateActionMenu(holder)
true
}
}
+ fun recycleCandidateViewHolder(holder: CandidateViewHolder) {
+ holder.itemView.setOnClickListener(null)
+ holder.itemView.setOnLongClickListener(null)
+ }
+
override fun onDetached() {
bar.expandButtonStateMachine.push(
ExpandedCandidatesDetached,
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt
index b63d4b0cb..180c52327 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.input.candidates.expanded.window
@@ -40,6 +40,11 @@ class FlexboxExpandedCandidateWindow :
super.onBindViewHolder(holder, position)
bindCandidateUiViewHolder(holder)
}
+
+ override fun onViewRecycled(holder: CandidateViewHolder) {
+ recycleCandidateViewHolder(holder)
+ super.onViewRecycled(holder)
+ }
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt
index dc365a5bf..23316d73f 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.input.candidates.expanded.window
@@ -35,6 +35,11 @@ class GridExpandedCandidateWindow :
super.onBindViewHolder(holder, position)
bindCandidateUiViewHolder(holder)
}
+
+ override fun onViewRecycled(holder: CandidateViewHolder) {
+ recycleCandidateViewHolder(holder)
+ super.onViewRecycled(holder)
+ }
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt
index dd5bb7bcb..69765841f 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.input.candidates.floating
@@ -94,6 +94,13 @@ class PagedCandidatesUi(
}
}
}
+
+ override fun onViewRecycled(holder: UiHolder) {
+ if (holder is UiHolder.Candidate) {
+ holder.ui.root.setOnClickListener(null)
+ }
+ super.onViewRecycled(holder)
+ }
}
private val candidatesLayoutManager = FlexboxLayoutManager(ctx).apply {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt
index 29719e4b1..b7ff4cd39 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.input.candidates.horizontal
@@ -29,7 +29,6 @@ import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.Ex
import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdated
import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent
import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver
-import org.fcitx.fcitx5.android.input.candidates.CandidateItemUi
import org.fcitx.fcitx5.android.input.candidates.CandidateViewHolder
import org.fcitx.fcitx5.android.input.candidates.expanded.decoration.FlexboxVerticalDecoration
import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateMode.AlwaysFillWidth
@@ -106,10 +105,16 @@ class HorizontalCandidateComponent :
fcitx.launchOnReady { it.select(holder.idx) }
}
holder.itemView.setOnLongClickListener {
- showCandidateActionMenu(holder.idx, candidates[position], holder.ui)
+ showCandidateActionMenu(holder)
true
}
}
+
+ override fun onViewRecycled(holder: CandidateViewHolder) {
+ holder.itemView.setOnClickListener(null)
+ holder.itemView.setOnLongClickListener(null)
+ super.onViewRecycled(holder)
+ }
}
}
@@ -206,14 +211,17 @@ class HorizontalCandidateComponent :
private var candidateActionMenu: PopupMenu? = null
- fun showCandidateActionMenu(idx: Int, text: String, ui: CandidateItemUi) {
+ fun showCandidateActionMenu(holder: CandidateViewHolder) {
+ val idx = holder.idx
+ val text = holder.text
+ val view = holder.ui.root
candidateActionMenu?.dismiss()
candidateActionMenu = null
service.lifecycleScope.launch {
val actions = fcitx.runOnReady { getCandidateActions(idx) }
if (actions.isEmpty()) return@launch
- InputFeedbacks.hapticFeedback(ui.root, longPress = true)
- candidateActionMenu = PopupMenu(context, ui.root).apply {
+ InputFeedbacks.hapticFeedback(view, longPress = true)
+ candidateActionMenu = PopupMenu(context, view).apply {
menu.add(buildSpannedString {
bold {
color(context.styledColor(android.R.attr.colorAccent)) {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingButton.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingButton.kt
index 356e7709c..49158d870 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingButton.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingButton.kt
@@ -13,7 +13,7 @@ import android.graphics.drawable.StateListDrawable
import androidx.annotation.DrawableRes
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView
-import org.fcitx.fcitx5.android.input.keyboard.borderedKeyBackgroundDrawable
+import org.fcitx.fcitx5.android.input.keyboard.shadowedKeyBackgroundDrawable
import org.fcitx.fcitx5.android.input.keyboard.insetRadiusDrawable
import org.fcitx.fcitx5.android.utils.borderDrawable
import org.fcitx.fcitx5.android.utils.pressHighlightDrawable
@@ -49,7 +49,7 @@ class TextEditingButton(
init {
if (bordered) {
val bkgColor = if (altStyle) theme.altKeyBackgroundColor else theme.keyBackgroundColor
- background = borderedKeyBackgroundDrawable(
+ background = shadowedKeyBackgroundDrawable(
bkgColor, theme.keyShadowColor,
radius, shadowWidth, hInset, vInset
)
@@ -126,14 +126,14 @@ class TextEditingButton(
StateListDrawable().apply {
addState(
intArrayOf(android.R.attr.state_activated),
- borderedKeyBackgroundDrawable(
+ shadowedKeyBackgroundDrawable(
theme.genericActiveBackgroundColor, theme.keyShadowColor,
radius, shadowWidth, hInset, vInset
)
)
addState(
intArrayOf(android.R.attr.state_enabled),
- borderedKeyBackgroundDrawable(
+ shadowedKeyBackgroundDrawable(
theme.keyBackgroundColor, theme.keyShadowColor,
radius, shadowWidth, hInset, vInset
)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt
index ce06a89b4..a5115fa93 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt
@@ -93,7 +93,7 @@ object EditorInfoParser {
private fun parseStringArray(arr: Any?): String {
if (arr == null) return NULL
- if (arr !is Array<*> || arr[0] !is String) return arr.toString()
+ if (arr !is Array<*>) return arr.toString()
return arr.joinToString()
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDrawable.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDrawable.kt
index f5ea1e1b5..7bb9476e2 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDrawable.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDrawable.kt
@@ -42,7 +42,7 @@ fun insetOvalDrawable(
hInset, vInset, hInset, vInset
)
-fun borderedKeyBackgroundDrawable(
+fun shadowedKeyBackgroundDrawable(
@ColorInt bkgColor: Int,
@ColorInt shadowColor: Int,
radius: Float,
@@ -58,3 +58,23 @@ fun borderedKeyBackgroundDrawable(
setLayerInset(0, hMargin, vMargin, hMargin, vMargin - shadowWidth)
setLayerInset(1, hMargin, vMargin, hMargin, vMargin)
}
+
+fun borderedKeyBackgroundDrawable(
+ @ColorInt bkgColor: Int,
+ @ColorInt shadowColor: Int,
+ radius: Float,
+ strokeWidth: Int,
+ hMargin: Int,
+ vMargin: Int
+): Drawable = LayerDrawable(
+ arrayOf(
+ GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = radius
+ setColor(bkgColor)
+ setStroke(strokeWidth, shadowColor)
+ }
+ )
+).apply {
+ setLayerInset(0, hMargin, vMargin, hMargin, vMargin)
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt
index cc1002ed3..25c0a2f65 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt
@@ -55,6 +55,7 @@ abstract class KeyView(ctx: Context, val theme: Theme, val def: KeyDef.Appearanc
CustomGestureView(ctx) {
val bordered: Boolean
+ val borderStroke: Boolean
val rippled: Boolean
val radius: Float
val hMargin: Int
@@ -63,6 +64,7 @@ abstract class KeyView(ctx: Context, val theme: Theme, val def: KeyDef.Appearanc
init {
val prefs = ThemeManager.prefs
bordered = prefs.keyBorder.getValue()
+ borderStroke = prefs.keyBorderStroke.getValue()
rippled = prefs.keyRippleEffect.getValue()
radius = dp(prefs.keyRadius.getValue().toFloat())
val landscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
@@ -122,11 +124,14 @@ abstract class KeyView(ctx: Context, val theme: Theme, val def: KeyDef.Appearanc
Variant.Alternative -> theme.altKeyBackgroundColor
Variant.Accent -> theme.accentKeyBackgroundColor
}
- val shadowWidth = dp(1)
+ val borderOrShadowWidth = dp(1)
// background: key border
- appearanceView.background = borderedKeyBackgroundDrawable(
+ appearanceView.background = if (borderStroke) borderedKeyBackgroundDrawable(
bkgColor, theme.keyShadowColor,
- radius, shadowWidth, hMargin, vMargin
+ radius, borderOrShadowWidth, hMargin, vMargin
+ ) else shadowedKeyBackgroundDrawable(
+ bkgColor, theme.keyShadowColor,
+ radius, borderOrShadowWidth, hMargin, vMargin
)
// foreground: press highlight or ripple
setupPressHighlight()
@@ -147,6 +152,16 @@ abstract class KeyView(ctx: Context, val theme: Theme, val def: KeyDef.Appearanc
// ripple should be masked with an opaque color
mask ?: highlightMaskDrawable(Color.WHITE)
)
+ } else if (bordered && borderStroke) {
+ StateListDrawable().apply {
+ addState(
+ intArrayOf(android.R.attr.state_pressed),
+ borderedKeyBackgroundDrawable(
+ Color.TRANSPARENT, theme.keyShadowColor,
+ radius, dp(2), hMargin, vMargin
+ )
+ )
+ }
} else {
StateListDrawable().apply {
addState(
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/common/BaseDynamicListUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/common/BaseDynamicListUi.kt
index 4f0d89fe9..ffb84da17 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/ui/common/BaseDynamicListUi.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/common/BaseDynamicListUi.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.common
@@ -13,6 +13,7 @@ import android.widget.CheckBox
import android.widget.ImageButton
import androidx.activity.OnBackPressedDispatcher
import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnAttach
@@ -21,9 +22,8 @@ import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.ItemTouchHelper
import arrow.core.identity
-import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
+import com.google.android.material.behavior.HideViewOnScrollBehavior
import com.google.android.material.floatingactionbutton.FloatingActionButton
-import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.utils.onPositiveButtonClick
@@ -174,15 +174,6 @@ abstract class BaseDynamicListUi(
action.invoke()
suspendUndo = false
}
- .addCallback(object : BaseTransientBottomBar.BaseCallback() {
- override fun onShown(transientBottomBar: Snackbar) {
- // snackbar is invisible when it attached to parent,
- // but change visibility won't trigger `onDependentViewChanged`.
- // so we need to update fab position when snackbar fully shown
- // see [^1]
- fab.translationY = -transientBottomBar.view.height.toFloat()
- }
- })
.show()
}
@@ -310,7 +301,12 @@ abstract class BaseDynamicListUi(
add(fab, defaultLParams {
gravity = gravityEndBottom
margin = dp(16)
- behavior = object : HideBottomViewOnScrollBehavior() {
+ behavior = object : HideViewOnScrollBehavior(EDGE_BOTTOM) {
+ /**
+ * the `translationY` FAB needs to be shown above the snackbar
+ */
+ var scrollInOffset = 0f
+
@SuppressLint("RestrictedApi")
override fun layoutDependsOn(
parent: CoordinatorLayout,
@@ -325,10 +321,16 @@ abstract class BaseDynamicListUi(
child: FloatingActionButton,
dependency: View
): Boolean {
+ // make sure FAB is above snackbar and doesn't go below bottom inset (eg. navbar)
+ scrollInOffset = min(0f, dependency.translationY - dependency.height)
// [^1]: snackbar is invisible when it attached to parent
- // update fab position only when snackbar is visible
- if (dependency.isVisible) {
- child.translationY = min(0f, dependency.translationY - dependency.height)
+ // update FAB position only when snackbar is visible
+ if (isScrolledIn && dependency.isVisible) {
+ // cancel pending animation to prevent translationY got overridden
+ // eg. when a new snackbar cancels previous snackbar, the canceled one would start
+ // an animation (as onDependentViewRemoved below) to update translationY constantly
+ child.animate().cancel()
+ child.translationY = scrollInOffset
return true
}
return false
@@ -339,7 +341,23 @@ abstract class BaseDynamicListUi(
child: FloatingActionButton,
dependency: View
) {
- child.translationY = 0f
+ // if the user dismiss the snackbar manually while FAB is visible,
+ // move the FAB back to its normal position
+ if (isScrolledIn) {
+ child.animate().translationY(0f)
+ }
+ }
+ }.apply {
+ addOnScrollStateChangedListener { view, _ ->
+ // overwrite FAB's position when scrolling in while snackbar is visible
+ if (isScrolledIn && scrollInOffset < 0f) {
+ // need to post because `HideViewOnScrollBehavior#slideIn` starts animation
+ // after notifying all onScrollStateChangedListeners
+ ContextCompat.getMainExecutor(ctx).execute {
+ view.clearAnimation()
+ view.animate().translationY(scrollInOffset)
+ }
+ }
}
}
})
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/KeyPreferenceUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/KeyPreferenceUi.kt
index 08b39782a..9bbdaffdd 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/KeyPreferenceUi.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/KeyPreferenceUi.kt
@@ -91,24 +91,19 @@ class KeyPreferenceUi(override val ctx: Context) : Ui {
imeOptions = EditorInfo.IME_FLAG_FORCE_ASCII
privateImeOptions = FcitxInputMethodService.DeleteSurroundingFlag
requestFocus()
- setOnKeyListener { _, _, event ->
- if (event.action != KeyEvent.ACTION_DOWN)
- return@setOnKeyListener false
- var sym = KeySym.fromKeyEvent(event)
- ?: return@setOnKeyListener false
- // convert lowercase latin to uppercase
+ setOnKeyListener l@{ _, _, event ->
+ if (event.action != KeyEvent.ACTION_DOWN) return@l false
+ val states = KeyStates.fromKeyEvent(event)
+ var sym = KeySym.fromKeyEvent(event) ?: return@l false
+ // convert lowercase latin to uppercase to make it look better
if (sym.sym in 0x61..0x7a) {
sym = KeySym(sym.sym - 0x20)
}
- val states = KeyStates.fromKeyEvent(event)
- val newKey = Key.create(sym, states)
- if (newKey.sym == Key.None.sym)
- return@setOnKeyListener false
- setKey(newKey)
- return@setOnKeyListener true
+ setKey(Key.create(sym, states))
+ return@l true
}
- addTextChangedListener {
- val text = it?.toString() ?: return@addTextChangedListener
+ addTextChangedListener l@{
+ val text = it?.toString() ?: return@l
val input = Key.parse(text)
setKey(Key.create(input.keySym, keyStates))
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/EditorInfo.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/EditorInfo.kt
new file mode 100644
index 000000000..17500488c
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/EditorInfo.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.utils
+
+import android.text.InputType
+import android.view.inputmethod.EditorInfo
+
+/**
+ * When the EditorInfo has [InputType.TYPE_NULL] and [EditorInfo.IME_NULL], it's likely not an input field
+ */
+fun EditorInfo.isTypeNull(): Boolean {
+ return inputType == InputType.TYPE_NULL && imeOptions == EditorInfo.IME_NULL
+}
diff --git a/app/src/main/play/release-notes/en-US/104.txt b/app/src/main/play/release-notes/en-US/104.txt
new file mode 100644
index 000000000..e8f6fbb9c
--- /dev/null
+++ b/app/src/main/play/release-notes/en-US/104.txt
@@ -0,0 +1,47 @@
+# 0.1.2 - Dynamic Color and Emoji Skin Tones
+
+## Highlights
+
+- Dynamic color themes for Android 12+, and monochrome variant of adaptive icon for Android 13+
+- New theme options: candidate window border radius, hide punctuation on keys, draw key border stroke instead of shadow
+- New option for Emoji Picker to hide unsupported emojis and change skin tone modifier
+
+### Notable changes
+
+- Recently used symbol and emojis are now saved as SharedPreference instead of plain text files, and will be migrated when opening the picker. Importing user data from older version will still work, but not vice versa
+- Improves compatibility for some devices with physical keyboard, eg. select candidate with combination keys
+
+### Build process improvements
+
+- Update Android NDK to r28, makes the app compatible with 16 KB page sizes
+- Migrate navigation graph xml to kotlin DSL
+- Make sure .mo files are reproducible across different gettext versions
+
+## New features
+
+- Dynamic color themes for Android 12+
+- Long press to share clipboard content
+- New preference for candidate window corner radius
+- Add option to hide punctuation on keyboard
+- Emoji skin tone selection and preference
+- Filter out unsupported emoji in picker
+- Add monochrome variant for adaptive icon (aka themed icon)
+- Allow changing verbose log option without restart
+- Add option to draw key border stroke instead of shadow
+
+## Bug fixes
+
+- Workaround some editors which misuse TYPE_TEXT_VARIATION_VISIBLE_PASSWORD as NoSpellCheck
+- Fix haptic feedback when amplitude not changed
+- Correct license of fcitx5-chewing
+- Don't show InputView on combination keys
+- Fix trigger candidate action in bulk candidate list
+- Fix long candidate line wrapping in vertical mode
+- Move CandidatesView above cursor only when there's larger space
+- Workaround modifier states for some phone keyboard
+- Fix CandidatesView OnGlobalLayoutListener memory leak
+- Fix ClearURLs redirection rules
+- Fix importErrorDialog message formatting
+- Tweak commitAndReset workaround for all languages
+- Fix "restart fcitx instance" in DeveloperFragment
+- Fix EditorInfoWindow crash on empty contentMimeTypes
diff --git a/app/src/main/play/release-notes/en-US/default.txt b/app/src/main/play/release-notes/en-US/default.txt
index 5f5cca6e1..18adfe29e 120000
--- a/app/src/main/play/release-notes/en-US/default.txt
+++ b/app/src/main/play/release-notes/en-US/default.txt
@@ -1 +1 @@
-84.txt
\ No newline at end of file
+104.txt
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 21b9d6001..c591c2796 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -106,8 +106,8 @@
キーの枠を有効にする
明るさ
キーの角半径
- キーの上下間隔
- キーの左右間隔
+ キーの左右間隔
+ キーの上下間隔
設定
画像を選択
黒字のキー
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index f291bdec5..98e6c3649 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -103,8 +103,8 @@
Перезагрузить конфигурацию
Параметры метода ввода
Тема
- Активироватьть эффект пульсации клавиш
- Активировать ограничение клавиш
+ Включить эффект пульсации клавиш
+ Включить границы клавиш
Яркость
Радиус клавиш
Радиус кнопки редактирования текста
@@ -122,6 +122,7 @@
Позиция пунктуации
Внизу (только портрет)
В правом верхнем углу
+ Нет
Развернуть панель инструментов по умолчанию
Всплывающее окно при нажатии клавиши
Отключить анимацию
@@ -270,6 +271,7 @@
Обрезать
Окно кандидатов
Показать окно кандидатов
+ Радиус окна кандидатов
Зависит от устройства ввода
Ориентация списка кандидатов
Автоматическая
@@ -303,4 +305,15 @@
Переместить курсор к концу
Открыть настройки метода ввода
Удалить всё
+ Поделиться
+ Эмодзи и символы
+ Модификатор оттенка кожи эмодзи по умолчанию
+ Без модификатора
+ 🏻 Type-1-2
+ 🏼 Type-3
+ 🏽 Type-4
+ 🏾 Type-5
+ 🏿 Type-6
+ Скрыть неподдерживаемые эмодзи
+ Включить обводку для границы клавиши
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 10600ea6d..4decba4b2 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -122,6 +122,7 @@
标点位置
底部(仅限竖屏)
右上
+ 无
默认展开工具栏
按键时弹出显示字符
禁用动画
@@ -269,7 +270,8 @@
垂直翻转
裁剪
候选窗口
- 显示候选词窗口
+ 显示候选窗口
+ 候选窗口圆角半径
根据输入设备而定
候选词列表方向
自动
@@ -303,14 +305,17 @@
把光标移动到结尾
打开输入法设置
全部删除
+ 分享
表情和符号
- 默认表情符号肤色修饰符
- 无修饰符
- 🏻 非常浅色
- 🏼 浅色
- 🏽 中等色
- 🏾 深色
- 🏿 非常深色
+ 默认表情肤色
+ 无
+ 🏻 浅肤色 (Type-1-2)
+ 🏼 中等浅肤色 (Type-3)
+ 🏽 中等肤色 (Type-4)
+ 🏾 中等深肤色 (Type-5)
+ 🏿 深肤色 (Type-6)
+ 隐藏不支持的表情
+ 启用按键边框描边
字体
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index fb474fe6a..3dec5bb62 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -122,6 +122,7 @@
標點符號的位置
底部(僅限直向)
右上
+ 無
預設展開工具列
按鍵時彈出顯示字元
停用動畫
@@ -270,6 +271,7 @@
裁剪
候選詞視窗
顯示候選詞視窗
+ 候選詞窗口半徑
視輸入設備而定
候選詞列表方向
自動
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2f76e9d5b..0f2495a87 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -315,6 +315,7 @@
🏾 Type-5
🏿 Type-6
Hide unsupported Emojis
+ Enable stroke for key border
Fonts
diff --git a/build-logic/convention/src/main/kotlin/AndroidAppConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidAppConventionPlugin.kt
index 01cd4b010..eb05595f6 100644
--- a/build-logic/convention/src/main/kotlin/AndroidAppConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidAppConventionPlugin.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
@@ -14,6 +14,7 @@ import org.gradle.api.Project
import org.gradle.api.file.RegularFile
import org.gradle.api.internal.provider.AbstractProperty
import org.gradle.api.internal.provider.Providers
+import org.gradle.api.plugins.BasePluginExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -25,6 +26,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
* - Provide default configuration for `android {...}`
* - Add desugar JDK libs
*/
+@Suppress("unused")
class AndroidAppConventionPlugin : AndroidBaseConventionPlugin() {
override fun apply(target: Project) {
@@ -133,7 +135,10 @@ class AndroidAppConventionPlugin : AndroidBaseConventionPlugin() {
}
// applicationId is not set upon apply
it.defaultConfig {
- target.setProperty("archivesBaseName", "$applicationId-$versionName")
+ // https://www.norio.be/blog/archivesBaseName-removed-from-gradle9.html
+ target.extensions.configure {
+ archivesName.set("$applicationId-$versionName")
+ }
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibConventionPlugin.kt
index 5651dd341..12f2a51bb 100644
--- a/build-logic/convention/src/main/kotlin/AndroidLibConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidLibConventionPlugin.kt
@@ -1,11 +1,12 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.gradle.tasks.ProcessLibraryArtProfileTask
import org.gradle.api.Project
import org.gradle.kotlin.dsl.withType
+@Suppress("unused")
class AndroidLibConventionPlugin : AndroidBaseConventionPlugin() {
override fun apply(target: Project) {
diff --git a/build-logic/convention/src/main/kotlin/AndroidPluginAppConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidPluginAppConventionPlugin.kt
index c2c70b8d6..87df99f6b 100644
--- a/build-logic/convention/src/main/kotlin/AndroidPluginAppConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidPluginAppConventionPlugin.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.api.Plugin
@@ -36,7 +36,7 @@ class AndroidPluginAppConventionPlugin : Plugin {
applicationVariants.all {
val pluginsTaskName = "assemble${name.capitalized()}Plugins"
val pluginsTask = target.rootProject.tasks.findByName(pluginsTaskName)
- ?: target.rootProject.task(pluginsTaskName)
+ ?: target.rootProject.tasks.register(pluginsTaskName).get()
pluginsTask.dependsOn(assembleProvider)
}
}
diff --git a/build-logic/convention/src/main/kotlin/BuildMetadataPlugin.kt b/build-logic/convention/src/main/kotlin/BuildMetadataPlugin.kt
index 988549cd0..4195fc008 100644
--- a/build-logic/convention/src/main/kotlin/BuildMetadataPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/BuildMetadataPlugin.kt
@@ -1,10 +1,9 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -12,11 +11,12 @@ import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.task
+import org.gradle.kotlin.dsl.register
/**
* Add task `generateBuildMetadata${Variant}`
*/
+@Suppress("unused")
class BuildMetadataPlugin : Plugin {
override fun apply(target: Project) {
@@ -32,7 +32,7 @@ class BuildMetadataPlugin : Plugin {
applicationVariants.all {
val variantName = name.capitalized()
target.afterEvaluate {
- target.task("generateBuildMetadata${variantName}") {
+ target.tasks.register("generateBuildMetadata${variantName}") {
val packageTask = packageApplicationProvider.get() // package${Variant} task
// create metadata file after package, because it's outputDirectory would
// be cleared at some time before package
diff --git a/build-logic/convention/src/main/kotlin/DataDescriptorPlugin.kt b/build-logic/convention/src/main/kotlin/DataDescriptorPlugin.kt
index a88fbb82c..48b2c6243 100644
--- a/build-logic/convention/src/main/kotlin/DataDescriptorPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/DataDescriptorPlugin.kt
@@ -1,9 +1,8 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -20,7 +19,7 @@ import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.create
-import org.gradle.kotlin.dsl.task
+import org.gradle.kotlin.dsl.register
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
@@ -28,7 +27,6 @@ import org.jetbrains.kotlin.com.google.common.hash.Hashing
import org.jetbrains.kotlin.com.google.common.io.ByteSource
import java.io.File
import java.nio.charset.Charset
-import kotlin.collections.set
interface DataDescriptorPluginExtension {
/**
@@ -58,13 +56,13 @@ class DataDescriptorPlugin : Plugin {
val extension = target.extensions.create(TASK)
extension.excludes.convention(listOf())
extension.symlinks.convention(mapOf())
- target.task(TASK) {
+ target.tasks.register(TASK) {
inputDir.set(target.assetsDir)
outputFile.set(target.assetsDir.resolve(FILE_NAME))
excludes.set(extension.excludes)
symlinks.set(extension.symlinks)
}
- target.task(CLEAN_TASK) {
+ target.tasks.register(CLEAN_TASK) {
delete(target.assetsDir.resolve(FILE_NAME))
}.also {
target.cleanTask.dependsOn(it)
diff --git a/build-logic/convention/src/main/kotlin/FcitxComponentPlugin.kt b/build-logic/convention/src/main/kotlin/FcitxComponentPlugin.kt
index 5f2c76210..eebdca6ee 100644
--- a/build-logic/convention/src/main/kotlin/FcitxComponentPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/FcitxComponentPlugin.kt
@@ -1,14 +1,16 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
+import com.android.build.gradle.tasks.ExternalNativeBuildJsonTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Delete
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.getByName
-import org.gradle.kotlin.dsl.task
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.withType
import java.io.File
import kotlin.io.path.isSymbolicLink
@@ -31,8 +33,8 @@ class FcitxComponentPlugin : Plugin {
}
override fun apply(target: Project) {
- val installTask = target.task(INSTALL_TASK)
- val deleteTask = target.task(DELETE_TASK)
+ val installTask = target.tasks.register(INSTALL_TASK)
+ val deleteTask = target.tasks.register(DELETE_TASK)
registerCMakeTask(target, "generate-desktop-file", "config")
registerCMakeTask(target, "translation-file", "translation")
registerCleanTask(target)
@@ -45,7 +47,7 @@ class FcitxComponentPlugin : Plugin {
registerCMakeTask(target, "translation-file", "translation", project)
}
if (ext.excludeFiles.isNotEmpty()) {
- deleteTask.apply {
+ deleteTask.get().apply {
dependsOn(installTask)
doLast {
ext.excludeFiles.forEach {
@@ -75,21 +77,15 @@ class FcitxComponentPlugin : Plugin {
} else {
"installLibrary$componentName[${sourceProject.name}]"
}
- val task = project.task(taskName) {
- runAfterNativeConfigure(sourceProject) { abiModel ->
- val cmake = abiModel.variant.module.cmake!!.cmakeExe!!
- if (target.isNotEmpty()) {
- sourceProject.exec {
- workingDir = abiModel.cxxBuildFolder
- commandLine(cmake, "--build", ".", "--target", target)
- }
- }
- sourceProject.exec {
- workingDir = abiModel.cxxBuildFolder
- environment("DESTDIR", project.assetsDir.absolutePath)
- commandLine(cmake, "--install", ".", "--component", component)
- }
- val ext = project.extensions.getByName("fcitxComponent")
+ val abiModel = sourceProject.getCxxAbiModelProperty()
+ val task = project.tasks.register(taskName) {
+ cxxAbiModel.set(abiModel)
+ buildTarget.set(target)
+ installComponent.set(component)
+ destDir.set(project.assetsDir)
+ mustRunAfter(sourceProject.tasks.withType())
+ doLast {
+ val ext = project.extensions.getByName(EXTENSION)
ext.modifyFiles.forEach { (path, function) ->
val file = project.assetsDir.resolve(path)
if (file.exists()) {
@@ -102,7 +98,7 @@ class FcitxComponentPlugin : Plugin {
}
private fun registerCleanTask(project: Project) {
- project.task(CLEAN_TASK) {
+ project.tasks.register(CLEAN_TASK) {
delete(project.assetsDir.resolve("usr/share/locale"))
// delete all non symlink files
// true -> delete
diff --git a/build-logic/convention/src/main/kotlin/FcitxHeadersPlugin.kt b/build-logic/convention/src/main/kotlin/FcitxHeadersPlugin.kt
index 33245b8e2..2b16edc17 100644
--- a/build-logic/convention/src/main/kotlin/FcitxHeadersPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/FcitxHeadersPlugin.kt
@@ -1,8 +1,9 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.android.build.gradle.tasks.ExternalNativeBuildJsonTask
import com.android.build.gradle.tasks.PrefabPackageConfigurationTask
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -10,10 +11,11 @@ import org.gradle.api.tasks.Delete
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.getByName
-import org.gradle.kotlin.dsl.task
+import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import java.io.File
+@Suppress("unused")
class FcitxHeadersPlugin : Plugin {
abstract class FcitxHeadersExtension {
@@ -55,25 +57,21 @@ class FcitxHeadersPlugin : Plugin {
}
private fun registerInstallTask(project: Project, name: String, component: String, dest: File) {
- val installHeadersTask = project.task(name) {
- runAfterNativeConfigure(project) { abiModel ->
- val cmake = abiModel.variant.module.cmake!!.cmakeExe!!
- project.exec {
- workingDir = abiModel.cxxBuildFolder
- environment("DESTDIR", dest.absolutePath)
- commandLine(cmake, "--install", ".", "--component", component)
- }
- }
+ val abiModel = project.getCxxAbiModelProperty()
+ val task = project.tasks.register(name) {
+ cxxAbiModel.set(abiModel)
+ installComponent.set(component)
+ destDir.set(dest)
+ mustRunAfter(project.tasks.withType())
}
-
// Make sure headers have been installed before configuring prefab package
project.tasks.withType().all {
- dependsOn(installHeadersTask)
+ dependsOn(task)
}
}
private fun registerCleanTask(project: Project) {
- project.task(CLEAN_TASK) {
+ project.tasks.register(CLEAN_TASK) {
delete(project.headersInstallDir)
delete(project.develComponentInstallDir)
}.also {
diff --git a/build-logic/convention/src/main/kotlin/NativeAppConventionPlugin.kt b/build-logic/convention/src/main/kotlin/NativeAppConventionPlugin.kt
index 09e2d5945..6e01be878 100644
--- a/build-logic/convention/src/main/kotlin/NativeAppConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/NativeAppConventionPlugin.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.FilterConfiguration.FilterType
@@ -8,6 +8,7 @@ import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
+@Suppress("unused")
class NativeAppConventionPlugin : NativeBaseConventionPlugin() {
override fun apply(target: Project) {
@@ -30,8 +31,10 @@ class NativeAppConventionPlugin : NativeBaseConventionPlugin() {
onVariants { variant ->
// different version code based on abi
variant.outputs.forEach { output ->
- val abi = output.filters.first { it.filterType == FilterType.ABI }.identifier
- output.versionCode.set(Versions.calculateVersionCode(abi))
+ val abi = output.filters.find { it.filterType == FilterType.ABI }
+ if (abi != null) {
+ output.versionCode.set(Versions.calculateVersionCode(abi.identifier))
+ }
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/NativeBaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/NativeBaseConventionPlugin.kt
index 494709f8c..1363db8f0 100644
--- a/build-logic/convention/src/main/kotlin/NativeBaseConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/NativeBaseConventionPlugin.kt
@@ -1,18 +1,21 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.Delete
-import org.gradle.kotlin.dsl.task
+import org.gradle.kotlin.dsl.register
open class NativeBaseConventionPlugin : Plugin {
override fun apply(target: Project) {
val prebuiltDir = target.rootProject.projectDir.resolve("lib/fcitx5/src/main/cpp/prebuilt")
+ val isBuildingBundle = target.rootProject.gradle.startParameter.taskNames.any {
+ it.startsWith("${target.path}:bundle")
+ }
target.extensions.configure(CommonExtension::class.java) {
ndkVersion = target.ndkVersion
defaultConfig {
@@ -34,12 +37,16 @@ open class NativeBaseConventionPlugin : Plugin {
path("src/main/cpp/CMakeLists.txt")
}
}
- splits.abi {
- isEnable = true
- isUniversalApk = false
- reset()
- (target.buildAbiOverride?.split(",") ?: Versions.supportedABIs).forEach {
- include(it)
+ // split apks should be disabled when building bundle
+ // https://issuetracker.google.com/issues/402800800
+ if (!isBuildingBundle) {
+ splits.abi {
+ isEnable = true
+ isUniversalApk = false
+ reset()
+ (target.buildAbiOverride?.split(",") ?: Versions.supportedABIs).forEach {
+ include(it)
+ }
}
}
}
@@ -47,7 +54,7 @@ open class NativeBaseConventionPlugin : Plugin {
}
private fun registerCleanCxxTask(project: Project) {
- project.task("cleanCxxIntermediates") {
+ project.tasks.register("cleanCxxIntermediates") {
delete(project.file(".cxx"))
}.also {
project.cleanTask.dependsOn(it)
diff --git a/build-logic/convention/src/main/kotlin/NativeBuildTasks.kt b/build-logic/convention/src/main/kotlin/NativeBuildTasks.kt
index 3e57272da..94fa32423 100644
--- a/build-logic/convention/src/main/kotlin/NativeBuildTasks.kt
+++ b/build-logic/convention/src/main/kotlin/NativeBuildTasks.kt
@@ -1,15 +1,24 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2024-2025 Fcitx5 for Android Contributors
*/
import com.android.build.gradle.internal.cxx.configure.rewriteWithLocations
import com.android.build.gradle.internal.cxx.logging.PassThroughRecordingLoggingEnvironment
import com.android.build.gradle.internal.cxx.model.CxxAbiModel
import com.android.build.gradle.tasks.ExternalNativeBuildJsonTask
import com.android.build.gradle.tasks.ExternalNativeBuildTask
+import org.gradle.api.Action
+import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.Task
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.TaskAction
+import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.withType
+import org.gradle.process.ExecSpec
+import java.io.File
fun ExternalNativeBuildJsonTask.abiModel(): CxxAbiModel {
val abi = ExternalNativeBuildJsonTask::class.java.declaredFields.find { it.name == "abi" }!!
@@ -18,29 +27,80 @@ fun ExternalNativeBuildJsonTask.abiModel(): CxxAbiModel {
}
/**
- * Important: make sure that the task runs after than the native task
- * Since we can't declare the dependency relationship, a weaker running order constraint must be enforced
+ * This function contains `configureEach` call, to avoid "task warming-up failure",
+ * **DO NOT** call this during other tasks' configuration
*/
-
-fun Task.runAfterNativeConfigure(project: Project, action: (abiModel: CxxAbiModel) -> Unit) {
- lateinit var abiModel: CxxAbiModel
- project.tasks.withType().all externalNativeBuild@{
- this@runAfterNativeConfigure.mustRunAfter(this@externalNativeBuild)
+fun Project.getCxxAbiModelProperty(): Property {
+ val abiModel: Property = project.objects.property()
+ tasks.withType().configureEach {
doFirst {
// `CxxAbiModel.rewriteWithLocations` requires a "Non-default logger"
// https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/cxx/configure/NativeLocationsBuildService.kt;drc=b5516899015633c99dc64b510d9729c4e001e89c;l=67
// just supply a random LoggingEnvironment or whatever this is
PassThroughRecordingLoggingEnvironment().use {
- abiModel = this@externalNativeBuild.abiModel()
- .rewriteWithLocations(nativeLocationsBuildService.get())
+ abiModel.set(
+ abiModel().rewriteWithLocations(nativeLocationsBuildService.get())
+ )
}
}
}
- doLast {
- action.invoke(abiModel)
+ return abiModel
+}
+
+abstract class CMakeBuildInstallTask : DefaultTask() {
+ @get:Input
+ @get:Optional
+ abstract val cxxAbiModel: Property
+
+ @get:Input
+ @get:Optional
+ abstract val buildTarget: Property
+
+ @get:Input
+ @get:Optional
+ abstract val installComponent: Property
+
+ @get:Input
+ abstract val destDir: Property
+
+ private fun exec(action: Action) {
+ project.providers.exec(action).result.get()
+ }
+
+ @TaskAction
+ fun execute() {
+ val cxxAbiModel = this.cxxAbiModel.get()
+ val buildTarget = this.buildTarget.getOrElse("")
+ val installComponent = this.installComponent.getOrElse("")
+ val destDir = this.destDir.get()
+ val cmake = cxxAbiModel.variant.module.cmake!!.cmakeExe!!
+ if (buildTarget.isNotEmpty()) {
+ exec {
+ commandLine(
+ cmake,
+ "--build", cxxAbiModel.cxxBuildFolder,
+ "--target", buildTarget
+ )
+ }
+ }
+ if (installComponent.isNotEmpty()) {
+ exec {
+ environment("DESTDIR", destDir.absolutePath)
+ commandLine(
+ cmake,
+ "--install", cxxAbiModel.cxxBuildFolder,
+ "--component", installComponent
+ )
+ }
+ }
}
}
+/**
+ * Important: make sure that the task runs after than the native task
+ * Since we can't declare the dependency relationship, a weaker running order constraint must be enforced
+ */
+
fun Task.runAfterNativeBuild(project: Project) {
project.tasks.withType().all externalNativeBuild@{
this@runAfterNativeBuild.mustRunAfter(this@externalNativeBuild)
diff --git a/build-logic/convention/src/main/kotlin/NativeLibConventionPlugin.kt b/build-logic/convention/src/main/kotlin/NativeLibConventionPlugin.kt
index 09f931427..2c798ef29 100644
--- a/build-logic/convention/src/main/kotlin/NativeLibConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/NativeLibConventionPlugin.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.tasks.PrefabPackageConfigurationTask
@@ -8,6 +8,7 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
+@Suppress("unused")
class NativeLibConventionPlugin : NativeBaseConventionPlugin() {
override fun apply(target: Project) {
diff --git a/build-logic/convention/src/main/kotlin/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/ProjectExtensions.kt
index bd1bdaa9d..215c60176 100644
--- a/build-logic/convention/src/main/kotlin/ProjectExtensions.kt
+++ b/build-logic/convention/src/main/kotlin/ProjectExtensions.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2024-2025 Fcitx5 for Android Contributors
*/
import com.android.build.gradle.internal.dsl.SigningConfig
@@ -9,20 +9,19 @@ import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.kotlin.dsl.the
-import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
fun Project.runCmd(cmd: String, defaultValue: String = ""): String {
- val stdout = ByteArrayOutputStream()
- val result = stdout.use {
- project.exec {
- commandLine = cmd.split(" ")
- standardOutput = stdout
- }
+ val output = providers.exec {
+ commandLine = cmd.split(" ")
+ }
+ return if (output.result.get().exitValue == 0) {
+ output.standardOutput.asText.get().trim()
+ } else {
+ defaultValue
}
- return if (result.exitValue == 0) stdout.toString().trim() else defaultValue
}
val Project.libs get() = the()
diff --git a/build-logic/convention/src/main/kotlin/Versions.kt b/build-logic/convention/src/main/kotlin/Versions.kt
index 6a9c1fd9d..54dce2e1e 100644
--- a/build-logic/convention/src/main/kotlin/Versions.kt
+++ b/build-logic/convention/src/main/kotlin/Versions.kt
@@ -17,8 +17,8 @@ object Versions {
const val defaultBuildTools = "35.0.1"
// NOTE: increase this value to bump version code
- const val baseVersionCode = 9
- const val baseVersionName = "0.1.1"
+ const val baseVersionCode = 10
+ const val baseVersionName = "0.1.2"
val supportedABIs = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
const val fallbackABI = "arm64-v8a"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 854ac264d..930bc794a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,20 +1,21 @@
[versions]
-androidGradlePlugin = "8.11.1"
-kotlin = "2.2.0"
-ksp = "2.2.0-2.0.2"
-lifecycle = "2.9.2"
-navigation = "2.9.2"
-room = "2.7.2"
+androidGradlePlugin = "8.13.0"
+kotlin = "2.2.21"
+ksp = "2.2.21-2.0.4"
+lifecycle = "2.9.4"
+navigation = "2.9.5"
+room = "2.8.3"
+test = "1.7.0"
splitties = "3.0.0"
-aboutlibraries = "12.2.4"
-arrow = "2.1.2"
+aboutlibraries = "13.1.0"
+arrow = "2.2.0"
[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
android-desugarJDKLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version = "2.1.5" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version = "1.10.2" }
-kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.8.1" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.9.0" }
androidx-activity = { module = "androidx.activity:activity-ktx", version = "1.10.1" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" }
androidx-autofill = { module = "androidx.autofill:autofill", version = "1.3.0" }
@@ -37,7 +38,7 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
androidx-startup = { module = "androidx.startup:startup-runtime", version = "1.2.0" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version = "1.1.0" }
-material = { module = "com.google.android.material:material", version = "1.12.0" }
+material = { module = "com.google.android.material:material", version = "1.13.0" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
arrow-functions = { module = "io.arrow-kt:arrow-functions", version.ref = "arrow" }
imagecropper = { module = "com.vanniktech:android-image-cropper", version = "4.6.0" }
@@ -56,8 +57,8 @@ splitties-views-recyclerview = { module = "com.louiscad.splitties:splitties-view
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
aboutlibraries-plugin = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlibraries" }
junit = { module = "junit:junit", version = "4.13.2" }
-androidx-test-runner = { module = "androidx.test:runner", version = "1.6.2" }
-androidx-test-rules = { module = "androidx.test:rules", version = "1.6.1" }
+androidx-test-runner = { module = "androidx.test:runner", version.ref = "test" }
+androidx-test-rules = { module = "androidx.test:rules", version.ref = "test" }
androidx-lifecycle-testing = { module = "androidx.lifecycle:lifecycle-runtime-testing", version.ref = "lifecycle" }
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version = "2.2.0" }
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
@@ -68,7 +69,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
-aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
+aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
-gitVersion = { id = "com.palantir.git-version", version = "4.0.0" }
+gitVersion = { id = "com.palantir.git-version", version = "4.1.0" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5147a0112..b5b154cdd 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sat Jun 28 02:09:40 CST 2025
+#Sun Nov 02 17:48:47 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons b/lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons
index 566be4f7e..d231148df 160000
--- a/lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons
+++ b/lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons
@@ -1 +1 @@
-Subproject commit 566be4f7e87caf64718dca7c1138d7b1eed85dd1
+Subproject commit d231148df40903a25d9c0cba89b0c48ade934289
diff --git a/lib/fcitx5/src/main/cpp/fcitx5 b/lib/fcitx5/src/main/cpp/fcitx5
index 155047095..8bdc4ec02 160000
--- a/lib/fcitx5/src/main/cpp/fcitx5
+++ b/lib/fcitx5/src/main/cpp/fcitx5
@@ -1 +1 @@
-Subproject commit 155047095247213e5d81faeb71efd2f0923b9f55
+Subproject commit 8bdc4ec023d8d3d33b8882b5938511d00a0b0b94
diff --git a/lib/libime/src/main/cpp/libime b/lib/libime/src/main/cpp/libime
index a6609b120..1bae69196 160000
--- a/lib/libime/src/main/cpp/libime
+++ b/lib/libime/src/main/cpp/libime
@@ -1 +1 @@
-Subproject commit a6609b1206bb036dffe8c2d8829f1448e54a97ad
+Subproject commit 1bae691968e002ea937b6fa8ec04dfc3360d3de1
diff --git a/plugin/anthy/licenses/libraries/fcitx5-anthy.json b/plugin/anthy/licenses/libraries/fcitx5-anthy.json
index a801df5e5..6af96e091 100644
--- a/plugin/anthy/licenses/libraries/fcitx5-anthy.json
+++ b/plugin/anthy/licenses/libraries/fcitx5-anthy.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-anthy",
- "artifactVersion": "5.1.6",
+ "artifactVersion": "5.1.8",
"description": "Anthy Wrapper for Fcitx",
"name": "fcitx/fcitx5-anthy",
"website": "https://github.com/fcitx/fcitx5-anthy",
diff --git a/plugin/anthy/src/main/cpp/fcitx5-anthy b/plugin/anthy/src/main/cpp/fcitx5-anthy
index 676cec867..b86260bdb 160000
--- a/plugin/anthy/src/main/cpp/fcitx5-anthy
+++ b/plugin/anthy/src/main/cpp/fcitx5-anthy
@@ -1 +1 @@
-Subproject commit 676cec8673b394cfd6a88797ccc366d68b432e4a
+Subproject commit b86260bdb8a3c9d0ed5a950976a734ad4a77d904
diff --git a/plugin/chewing/licenses/libraries/fcitx5-chewing.json b/plugin/chewing/licenses/libraries/fcitx5-chewing.json
index 6f8e0db8e..a9c45de37 100644
--- a/plugin/chewing/licenses/libraries/fcitx5-chewing.json
+++ b/plugin/chewing/licenses/libraries/fcitx5-chewing.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-chewing",
- "artifactVersion": "5.1.7",
+ "artifactVersion": "5.1.9",
"description": "Chewing Wrapper for Fcitx",
"name": "fcitx/fcitx5-chewing",
"website": "https://github.com/fcitx/fcitx5-chewing",
diff --git a/plugin/chewing/src/main/cpp/fcitx5-chewing b/plugin/chewing/src/main/cpp/fcitx5-chewing
index c8ad8e2ca..26b8c5fa6 160000
--- a/plugin/chewing/src/main/cpp/fcitx5-chewing
+++ b/plugin/chewing/src/main/cpp/fcitx5-chewing
@@ -1 +1 @@
-Subproject commit c8ad8e2ca56fd828e44702123da7fc71be5dff5d
+Subproject commit 26b8c5fa67852763ee24a466bcf6cd53d6c5b20a
diff --git a/plugin/hangul/licenses/libraries/fcitx5-hangul.json b/plugin/hangul/licenses/libraries/fcitx5-hangul.json
index e73c0bc66..b43e8d8dc 100644
--- a/plugin/hangul/licenses/libraries/fcitx5-hangul.json
+++ b/plugin/hangul/licenses/libraries/fcitx5-hangul.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-hangul",
- "artifactVersion": "5.1.6",
+ "artifactVersion": "5.1.8",
"description": "Hangul Wrapper for Fcitx",
"name": "fcitx/fcitx5-hangul",
"website": "https://github.com/fcitx/fcitx5-hangul",
diff --git a/plugin/hangul/src/main/cpp/fcitx5-hangul b/plugin/hangul/src/main/cpp/fcitx5-hangul
index 69e346599..cc59c0a95 160000
--- a/plugin/hangul/src/main/cpp/fcitx5-hangul
+++ b/plugin/hangul/src/main/cpp/fcitx5-hangul
@@ -1 +1 @@
-Subproject commit 69e346599ed1ae83c62fc09d65741be1fea7b8f5
+Subproject commit cc59c0a95aba6349f5b0c753965ccaab3795ccd4
diff --git a/plugin/jyutping/licenses/libraries/libime-jyutping.json b/plugin/jyutping/licenses/libraries/libime-jyutping.json
index 6bc71da2b..fea65e8c0 100644
--- a/plugin/jyutping/licenses/libraries/libime-jyutping.json
+++ b/plugin/jyutping/licenses/libraries/libime-jyutping.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/libime-jyutping",
- "artifactVersion": "1.0.13",
+ "artifactVersion": "1.0.15",
"description": "Jyutping (Cantonese Input Method) engine support for libime",
"name": "fcitx/libime-jyutping",
"website": "https://github.com/fcitx/libime-jyutping",
diff --git a/plugin/jyutping/src/main/cpp/libime-jyutping b/plugin/jyutping/src/main/cpp/libime-jyutping
index c4e50525d..8e3e80217 160000
--- a/plugin/jyutping/src/main/cpp/libime-jyutping
+++ b/plugin/jyutping/src/main/cpp/libime-jyutping
@@ -1 +1 @@
-Subproject commit c4e50525d6b40b48f82e682228a374cd6cf91ed3
+Subproject commit 8e3e80217bc09d0c93e44652d6d768fb3be2b69d
diff --git a/plugin/rime/licenses/libraries/boost.json b/plugin/rime/licenses/libraries/boost.json
index cebc09b62..e35fe05d8 100644
--- a/plugin/rime/licenses/libraries/boost.json
+++ b/plugin/rime/licenses/libraries/boost.json
@@ -1,6 +1,6 @@
{
"uniqueId": "boostorg/boost",
- "artifactVersion": "1.86.0",
+ "artifactVersion": "1.87.0",
"description": "Free peer-reviewed portable C++ source libraries",
"name": "boostorg/boost",
"website": "https://www.boost.org/",
diff --git a/plugin/rime/licenses/libraries/fcitx5-rime.json b/plugin/rime/licenses/libraries/fcitx5-rime.json
index 90b91f90c..0e13f175c 100644
--- a/plugin/rime/licenses/libraries/fcitx5-rime.json
+++ b/plugin/rime/licenses/libraries/fcitx5-rime.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-rime",
- "artifactVersion": "5.1.10",
+ "artifactVersion": "5.1.12",
"description": "Rime Wrapper for Fcitx",
"name": "fcitx/fcitx5-rime",
"website": "https://github.com/fcitx/fcitx5-rime",
diff --git a/plugin/rime/licenses/libraries/librime.json b/plugin/rime/licenses/libraries/librime.json
index a9386cc07..d7f8c4cb0 100644
--- a/plugin/rime/licenses/libraries/librime.json
+++ b/plugin/rime/licenses/libraries/librime.json
@@ -1,6 +1,6 @@
{
"uniqueId": "rime/librime",
- "artifactVersion": "1.12.0",
+ "artifactVersion": "1.13.1",
"description": "Rime Input Method Engine",
"name": "rime/librime",
"website": "https://github.com/rime/librime",
diff --git a/plugin/rime/licenses/libraries/rime-essay.json b/plugin/rime/licenses/libraries/rime-essay.json
index 528cab4cd..95095d889 100644
--- a/plugin/rime/licenses/libraries/rime-essay.json
+++ b/plugin/rime/licenses/libraries/rime-essay.json
@@ -1,6 +1,6 @@
{
"uniqueId": "rime/rime-essay",
- "artifactVersion": "13674aa",
+ "artifactVersion": "943c7bf",
"description": "The shared vocabulary and language model",
"name": "rime/rime-essay",
"website": "https://github.com/rime/rime-essay",
diff --git a/plugin/rime/licenses/libraries/rime-luna-pinyin.json b/plugin/rime/licenses/libraries/rime-luna-pinyin.json
index 15ad7a4c9..9e23a1e4a 100644
--- a/plugin/rime/licenses/libraries/rime-luna-pinyin.json
+++ b/plugin/rime/licenses/libraries/rime-luna-pinyin.json
@@ -1,6 +1,6 @@
{
"uniqueId": "rime/rime-luna-pinyin",
- "artifactVersion": "b7db6ce",
+ "artifactVersion": "46acf03",
"description": "Rime 預設的拼音輸入方案",
"name": "rime/rime-luna-pinyin",
"website": "https://github.com/rime/rime-luna-pinyin",
diff --git a/plugin/rime/licenses/libraries/rime-prelude.json b/plugin/rime/licenses/libraries/rime-prelude.json
index c90a79502..7f5c9f837 100644
--- a/plugin/rime/licenses/libraries/rime-prelude.json
+++ b/plugin/rime/licenses/libraries/rime-prelude.json
@@ -1,6 +1,6 @@
{
"uniqueId": "rime/rime-prelude",
- "artifactVersion": "3803f09",
+ "artifactVersion": "3c602fd",
"description": "Essential files for building up your Rime configuration",
"name": "rime/rime-prelude",
"website": "https://github.com/rime/rime-prelude",
diff --git a/plugin/rime/licenses/libraries/rime-stroke.json b/plugin/rime/licenses/libraries/rime-stroke.json
index 07a4d2659..b5c8632db 100644
--- a/plugin/rime/licenses/libraries/rime-stroke.json
+++ b/plugin/rime/licenses/libraries/rime-stroke.json
@@ -1,6 +1,6 @@
{
"uniqueId": "rime/rime-stroke",
- "artifactVersion": "7c9874c",
+ "artifactVersion": "3a4b0f4",
"description": "Rime 五筆畫輸入方案",
"name": "rime/rime-stroke",
"website": "https://github.com/rime/rime-stroke",
diff --git a/plugin/rime/src/main/cpp/fcitx5-rime b/plugin/rime/src/main/cpp/fcitx5-rime
index d76a3c345..52b21180b 160000
--- a/plugin/rime/src/main/cpp/fcitx5-rime
+++ b/plugin/rime/src/main/cpp/fcitx5-rime
@@ -1 +1 @@
-Subproject commit d76a3c345332f1aa32cf6f4fd0c8189bf222531f
+Subproject commit 52b21180baf59f3f890229d9a6d4e1d81be37ecb
diff --git a/plugin/rime/src/main/cpp/rime-essay b/plugin/rime/src/main/cpp/rime-essay
index b3b4a614b..943c7bff0 160000
--- a/plugin/rime/src/main/cpp/rime-essay
+++ b/plugin/rime/src/main/cpp/rime-essay
@@ -1 +1 @@
-Subproject commit b3b4a614bde0dddd309200caaa8cb8827a7459e5
+Subproject commit 943c7bff037c163a80c452bd7ac80cd2b01975e6
diff --git a/plugin/rime/src/main/cpp/rime-luna-pinyin b/plugin/rime/src/main/cpp/rime-luna-pinyin
index 959309008..46acf0314 160000
--- a/plugin/rime/src/main/cpp/rime-luna-pinyin
+++ b/plugin/rime/src/main/cpp/rime-luna-pinyin
@@ -1 +1 @@
-Subproject commit 9593090080294a7dfc3c864c087e4070459c0168
+Subproject commit 46acf03142c12b5aeed7002675046bf7255eed35
diff --git a/plugin/rime/src/main/cpp/rime-stroke b/plugin/rime/src/main/cpp/rime-stroke
index 7c9874c6b..3a4b0f401 160000
--- a/plugin/rime/src/main/cpp/rime-stroke
+++ b/plugin/rime/src/main/cpp/rime-stroke
@@ -1 +1 @@
-Subproject commit 7c9874c6b2e0b94947653e9a7de6f99623ff27e4
+Subproject commit 3a4b0f4013e2b4c14b1e80c92b1d4723eb65f39c
diff --git a/plugin/sayura/licenses/libraries/fcitx5-sayura.json b/plugin/sayura/licenses/libraries/fcitx5-sayura.json
index 9fe00962d..fa39780b1 100644
--- a/plugin/sayura/licenses/libraries/fcitx5-sayura.json
+++ b/plugin/sayura/licenses/libraries/fcitx5-sayura.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-sayura",
- "artifactVersion": "5.1.3",
+ "artifactVersion": "5.1.5",
"description": "Sinhala input method for Fcitx",
"name": "fcitx/fcitx5-sayura",
"website": "https://github.com/fcitx/fcitx5-sayura",
diff --git a/plugin/sayura/src/main/cpp/fcitx5-sayura b/plugin/sayura/src/main/cpp/fcitx5-sayura
index 1a213cd15..4cfb7ec0f 160000
--- a/plugin/sayura/src/main/cpp/fcitx5-sayura
+++ b/plugin/sayura/src/main/cpp/fcitx5-sayura
@@ -1 +1 @@
-Subproject commit 1a213cd15485cdbc827d7dd7bfab082dca441b3d
+Subproject commit 4cfb7ec0f54ac6680840fd1f4ec3a60cab21aad8
diff --git a/plugin/thai/licenses/libraries/fcitx5-libthai.json b/plugin/thai/licenses/libraries/fcitx5-libthai.json
index a02b34b40..0de45c0cb 100644
--- a/plugin/thai/licenses/libraries/fcitx5-libthai.json
+++ b/plugin/thai/licenses/libraries/fcitx5-libthai.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-libthai",
- "artifactVersion": "5.1.5",
+ "artifactVersion": "5.1.7",
"description": "Thai Wrapper for Fcitx",
"name": "fcitx/fcitx5-libthai",
"website": "https://github.com/fcitx/fcitx5-libthai",
diff --git a/plugin/thai/licenses/libraries/libiconv.json b/plugin/thai/licenses/libraries/libiconv.json
index a12152603..528a42002 100644
--- a/plugin/thai/licenses/libraries/libiconv.json
+++ b/plugin/thai/licenses/libraries/libiconv.json
@@ -1,6 +1,6 @@
{
"uniqueId": "GNU/libiconv",
- "artifactVersion": "1.17",
+ "artifactVersion": "1.18",
"description": "Text encoding conversion library",
"name": "GNU/libiconv",
"website": "https://savannah.gnu.org/projects/libiconv",
diff --git a/plugin/thai/src/main/cpp/fcitx5-libthai b/plugin/thai/src/main/cpp/fcitx5-libthai
index 22ccdf4d6..334b2a972 160000
--- a/plugin/thai/src/main/cpp/fcitx5-libthai
+++ b/plugin/thai/src/main/cpp/fcitx5-libthai
@@ -1 +1 @@
-Subproject commit 22ccdf4d6a35898637c7d8addf74cc4e3dd36a4d
+Subproject commit 334b2a972245eaa412cd5c6c7c52c6edf7fd520f
diff --git a/plugin/thai/src/main/play/listings/en-US/title.txt b/plugin/thai/src/main/play/listings/en-US/title.txt
index fc834752b..fdfca4448 100644
--- a/plugin/thai/src/main/play/listings/en-US/title.txt
+++ b/plugin/thai/src/main/play/listings/en-US/title.txt
@@ -1 +1 @@
-Fcitx5 (That Plugin)
\ No newline at end of file
+Fcitx5 (Thai Plugin)
diff --git a/plugin/thai/src/main/play/listings/ru/short-description.txt b/plugin/thai/src/main/play/listings/ru/short-description.txt
new file mode 100644
index 000000000..3d915abd4
--- /dev/null
+++ b/plugin/thai/src/main/play/listings/ru/short-description.txt
@@ -0,0 +1 @@
+Тайский метод ввода для Fcitx5
diff --git a/plugin/thai/src/main/play/listings/ru/title.txt b/plugin/thai/src/main/play/listings/ru/title.txt
new file mode 100644
index 000000000..eb56fd547
--- /dev/null
+++ b/plugin/thai/src/main/play/listings/ru/title.txt
@@ -0,0 +1 @@
+Fcitx5 (Плагин Thai)
diff --git a/plugin/unikey/licenses/libraries/fcitx5-unikey.json b/plugin/unikey/licenses/libraries/fcitx5-unikey.json
index 7d7c2607e..b497307da 100644
--- a/plugin/unikey/licenses/libraries/fcitx5-unikey.json
+++ b/plugin/unikey/licenses/libraries/fcitx5-unikey.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-unikey",
- "artifactVersion": "5.1.6",
+ "artifactVersion": "5.1.8",
"description": "Unikey (Vietnamese Input Method) engine support for Fcitx",
"name": "fcitx/fcitx5-unikey",
"website": "https://github.com/fcitx/fcitx5-unikey",
diff --git a/plugin/unikey/src/main/cpp/fcitx5-unikey b/plugin/unikey/src/main/cpp/fcitx5-unikey
index c11b64ca1..e5662d387 160000
--- a/plugin/unikey/src/main/cpp/fcitx5-unikey
+++ b/plugin/unikey/src/main/cpp/fcitx5-unikey
@@ -1 +1 @@
-Subproject commit c11b64ca1572ef62f1da5f1f84e53ecb2c6c29df
+Subproject commit e5662d38785c904064de46330c0c6d73afbda7d0