diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..e02a1352b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,194 @@ +name: Commit CI + +on: + workflow_dispatch: + push: + branches: + - '*' + tags: + - '*' # 允许所有标签触发构建 + paths: + - '**/**' + - '!*.md' + - '!.gitignore' + +jobs: + build_commit: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-22.04 + #- macos-13 + #- windows-2022 + abi: + #- armeabi-v7a + - arm64-v8a + #- x86 + #- x86_64 + env: + BUILD_ABI: ${{ matrix.abi }} + steps: + - name: Fetch source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + #- name: Regenerate symlinks pointing to submodule (Windows) + # if: ${{ matrix.os == 'windows-2022' }} + # run: | + # Remove-Item -Recurse app/src/main/assets/usr/share + # git checkout -- * + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Setup Android environment + uses: android-actions/setup-android@v3 + + - name: Install Android NDK + run: | + sdkmanager --install "cmake;3.22.1" + + - name: Install system dependencies (Ubuntu) + if: ${{ matrix.os == 'ubuntu-22.04' }} + run: | + sudo apt update + sudo apt install extra-cmake-modules gettext + + #- name: Install system dependencies (macOS) + # if: ${{ matrix.os == 'macos-13' }} + # run: | + # brew install extra-cmake-modules + + #- name: Install system dependencies (Windows) + # if: ${{ matrix.os == 'windows-2022' }} + # run: | + # C:/msys64/usr/bin/pacman -S --noconfirm mingw-w64-ucrt-x86_64-gettext mingw-w64-ucrt-x86_64-extra-cmake-modules + # Add-Content $env:GITHUB_PATH "C:/msys64/ucrt64/bin" + + - name: Prepare personal build sources + run: | + ./prepare_personal_build.sh + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Build Release APK + run: | + ./gradlew :app:assembleRelease + ./gradlew :assembleReleasePlugins + + # move plugin apks to app/build/outputs/apk/release + - name: Move plugin APKs + shell: bash + run: | + for i in $(ls plugin) + do + if [ -d "plugin/${i}" ] + then + mv "plugin/${i}/build/outputs/apk/release"/*.apk "app/build/outputs/apk/release/" + fi + done + + - name: Sign all App + uses: kevin-david/zipalign-sign-android-release@v1.1.1 + id: sign_apk + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + alias: ${{ secrets.KEY_ALIAS }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + zipAlign: true + env: + BUILD_TOOLS_VERSION: "31.0.0" + + - name: Delete unsigned APKs + shell: bash + run: | + rm app/build/outputs/apk/release/*unsigned.apk + pushd app/build/outputs/apk/release + for file in *unsigned-*; do + new_name="${file/unsigned-/}" + mv -- "$file" "$new_name" + done + popd + + - name: Upload app with plugins + uses: actions/upload-artifact@v4 + with: + name: app-${{ matrix.os }}-${{ matrix.abi }}-with-plugins + path: app/build/outputs/apk/release/*.apk + + # create nightly release + - name: Generate unique release tag + id: generate_tag + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + # 如果是 tag 触发,使用 tag 名称 + 时间戳确保唯一性 + RELEASE_TAG="${{ github.ref_name }}-$(date +'%Y%m%d-%H%M%S')" + RELEASE_TITLE="Release ${{ github.ref_name }} ($(date +'%Y-%m-%d %H:%M:%S'))" + IS_PRERELEASE="false" + else + # 如果是分支触发,创建 nightly 构建 + RELEASE_TAG="nightly-${{ github.ref_name }}-$(date +'%Y%m%d-%H%M%S')" + RELEASE_TITLE="Nightly Build - ${{ github.ref_name }} ($(date +'%Y-%m-%d %H:%M:%S'))" + IS_PRERELEASE="true" + fi + echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + echo "release_title=$RELEASE_TITLE" >> $GITHUB_OUTPUT + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + + - name: Generate release notes + id: generate_notes + run: | + # 获取最近的 commits 作为 release notes + if [[ "${{ github.ref_type }}" == "tag" ]]; then + # 对于 tag,获取从上一个 tag 到当前 tag 的变更 + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [[ -n "$PREVIOUS_TAG" ]]; then + RELEASE_NOTES=$(git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD) + else + RELEASE_NOTES=$(git log --pretty=format:"- %s (%h)" --max-count=10) + fi + else + # 对于分支,获取最近 10 个 commits + RELEASE_NOTES=$(git log --pretty=format:"- %s (%h)" --max-count=10) + fi + + # 添加构建信息 + BUILD_INFO=" + + ## 构建信息 + - 构建时间: $(date +'%Y-%m-%d %H:%M:%S UTC') + - 分支/标签: ${{ github.ref_name }} + - 提交: ${{ github.sha }} + - 构建系统: ${{ matrix.os }} + - 架构: ${{ matrix.abi }} + " + + FULL_NOTES="## 更改日志 + $RELEASE_NOTES + $BUILD_INFO" + + # 保存到文件,处理多行内容 + echo "$FULL_NOTES" > release_notes.md + echo "Generated release notes:" + cat release_notes.md + + - name: Create Release + uses: 'marvinpinto/action-automatic-releases@latest' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + automatic_release_tag: ${{ steps.generate_tag.outputs.release_tag }} + prerelease: ${{ steps.generate_tag.outputs.is_prerelease }} + title: ${{ steps.generate_tag.outputs.release_title }} + body_path: release_notes.md + files: | + app/build/outputs/apk/release/*.apk + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..7fb8d5030 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,231 @@ +name: Release + +on: + push: + tags: + - 'v[0-9]+.*' # 匹配版本标签,如 v1.0.0, v2.1.3-beta 等 + workflow_dispatch: + inputs: + tag: + description: 'Tag name for release' + required: true + default: 'v0.0.0' + prerelease: + description: 'Mark as pre-release' + type: boolean + default: false + +jobs: + release: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-22.04 + abi: + - arm64-v8a + - armeabi-v7a + - x86_64 + env: + BUILD_ABI: ${{ matrix.abi }} + steps: + - name: Fetch source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Setup Android environment + uses: android-actions/setup-android@v3 + + - name: Install Android NDK + run: | + sdkmanager --install "cmake;3.22.1" + + - name: Install system dependencies + run: | + sudo apt update + sudo apt install extra-cmake-modules gettext + + - name: Prepare personal build sources + run: | + ./prepare_personal_build.sh + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Build Release APK + run: | + ./gradlew :app:assembleRelease + ./gradlew :assembleReleasePlugins + + - name: Move plugin APKs + shell: bash + run: | + for i in $(ls plugin) + do + if [ -d "plugin/${i}" ] + then + mv "plugin/${i}/build/outputs/apk/release"/*.apk "app/build/outputs/apk/release/" + fi + done + + - name: Sign all App + uses: kevin-david/zipalign-sign-android-release@v1.1.1 + id: sign_apk + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + alias: ${{ secrets.KEY_ALIAS }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + zipAlign: true + env: + BUILD_TOOLS_VERSION: "31.0.0" + + - name: Delete unsigned APKs + shell: bash + run: | + rm app/build/outputs/apk/release/*unsigned.apk + pushd app/build/outputs/apk/release + for file in *unsigned-*; do + new_name="${file/unsigned-/}" + mv -- "$file" "$new_name" + done + popd + + - name: Determine release info + id: release_info + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + TAG_NAME="${{ github.event.inputs.tag }}" + IS_PRERELEASE="${{ github.event.inputs.prerelease }}" + else + TAG_NAME="${{ github.ref_name }}" + # 判断是否为预发布版本(包含 alpha, beta, rc 等) + if [[ "$TAG_NAME" =~ (alpha|beta|rc|pre) ]]; then + IS_PRERELEASE="true" + else + IS_PRERELEASE="false" + fi + fi + + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + echo "release_title=Release $TAG_NAME" >> $GITHUB_OUTPUT + + - name: Generate comprehensive release notes + id: release_notes + run: | + TAG_NAME="${{ steps.release_info.outputs.tag_name }}" + + # 获取上一个版本标签 + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + + # 生成变更日志 + if [[ -n "$PREVIOUS_TAG" ]]; then + echo "# 🚀 Release $TAG_NAME" > release_notes.md + echo "" >> release_notes.md + echo "## 📝 更改内容" >> release_notes.md + echo "" >> release_notes.md + + # 获取提交信息并分类 + git log --pretty=format:"%s (%h)" $PREVIOUS_TAG..HEAD | while read line; do + if [[ "$line" =~ ^(feat|feature) ]]; then + echo "### ✨ 新功能" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(fix|bugfix) ]]; then + echo "### 🐛 问题修复" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(docs|doc) ]]; then + echo "### 📚 文档更新" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(style|ui) ]]; then + echo "### 💄 界面改进" >> release_notes.md + echo "- $line" >> release_notes.md + elif [[ "$line" =~ ^(perf|performance) ]]; then + echo "### ⚡ 性能优化" >> release_notes.md + echo "- $line" >> release_notes.md + else + echo "### 🔧 其他更改" >> release_notes.md + echo "- $line" >> release_notes.md + fi + done + else + echo "# 🚀 Release $TAG_NAME" > release_notes.md + echo "" >> release_notes.md + echo "## 📝 初始版本" >> release_notes.md + echo "" >> release_notes.md + echo "这是项目的初始发布版本。" >> release_notes.md + fi + + # 添加构建信息 + echo "" >> release_notes.md + echo "## 🏗️ 构建信息" >> release_notes.md + echo "" >> release_notes.md + echo "- **构建时间**: $(date +'%Y-%m-%d %H:%M:%S UTC')" >> release_notes.md + echo "- **构建版本**: $TAG_NAME" >> release_notes.md + echo "- **提交 SHA**: \`${{ github.sha }}\`" >> release_notes.md + echo "- **构建架构**: ${{ matrix.abi }}" >> release_notes.md + + # 添加安装说明 + echo "" >> release_notes.md + echo "## 📱 安装说明" >> release_notes.md + echo "" >> release_notes.md + echo "1. 下载对应架构的 APK 文件" >> release_notes.md + echo "2. 在 Android 设备上启用「未知来源应用安装」" >> release_notes.md + echo "3. 安装主应用 APK" >> release_notes.md + echo "4. 根据需要安装插件 APK" >> release_notes.md + + echo "Generated release notes:" + cat release_notes.md + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts-${{ matrix.abi }} + path: app/build/outputs/apk/release/*.apk + + - name: Create Release + if: matrix.abi == 'arm64-v8a' # 只在一个架构上创建 release,避免重复 + uses: 'marvinpinto/action-automatic-releases@latest' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + automatic_release_tag: ${{ steps.release_info.outputs.tag_name }} + prerelease: ${{ steps.release_info.outputs.is_prerelease }} + title: ${{ steps.release_info.outputs.release_title }} + body_path: release_notes.md + files: | + app/build/outputs/apk/release/*.apk + + collect-artifacts: + if: always() + needs: release + runs-on: ubuntu-22.04 + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect all APKs + run: | + mkdir -p collected-apks + find artifacts -name "*.apk" -exec cp {} collected-apks/ \; + ls -la collected-apks/ + + - name: Update Release with all artifacts + if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch' + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name || github.event.inputs.tag }} + files: collected-apks/*.apk + fail_on_unmatched_files: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/FONT_FEATURE_README.md b/FONT_FEATURE_README.md new file mode 100644 index 000000000..36a492461 --- /dev/null +++ b/FONT_FEATURE_README.md @@ -0,0 +1,132 @@ +# Fcitx5 Android 字体配置功能实现 + +## 概述 + +本功能为 Fcitx5-android 项目添加了完整的字体配置管理功能,允许用户在设置界面中直接选择和配置不同组件的字体。 + +## 功能特性 + +### 支持的字体配置项 +- **候选字体** (`cand_font`): 候选词显示字体 +- **编码字体** (`preedit_font`): 输入编码显示字体 +- **弹出按键字体** (`popup_key_font`): 弹出键盘按键字体 +- **按键主字体** (`key_main_font`): 主要按键文字字体 +- **按键次字体** (`key_alt_font`): 按键辅助文字字体 +- **默认字体** (`font`): AutoScaleTextView 默认字体 + +### 核心功能 +1. **字体文件管理**: 自动扫描 `fonts/` 目录下的 `.ttf` 和 `.otf` 字体文件 +2. **实时配置**: 配置更改后立即生效,无需重启应用 +3. **配置持久化**: 自动保存配置到 `fonts/fontset.json` 文件 +4. **系统默认支持**: 可选择使用系统默认字体 + +## 实现细节 + +### 文件结构 + +``` +app/src/main/java/org/fcitx/fcitx5/android/ +├── data/ +│ ├── fonts/ +│ │ ├── FontManager.kt # 字体管理核心类 +│ │ └── FontConfigTest.kt # 测试工具类 +│ └── prefs/ +│ └── AppPrefs.kt # 添加了 Fonts 配置类 +└── ui/main/settings/behavior/ + └── FontsSettingsFragment.kt # 字体设置界面 +``` + +### 核心类说明 + +#### FontManager +- 管理字体文件扫描和字体选择对话框 +- 处理字体配置的读写和更新 +- 提供字体缓存清理功能 + +#### AppPrefs.Fonts +- 管理字体配置的 SharedPreferences 存储 +- 监听配置更改并触发自动更新 + +#### FontsSettingsFragment +- 提供用户友好的字体选择界面 +- 显示当前选择的字体名称 +- 集成字体选择对话框 + +### 配置文件格式 + +字体配置保存在 `fonts/fontset.json` 文件中: + +```json +{ + "cand_font": "LXGWWenKai-Regular.ttf", + "font": "", + "preedit_font": "segoepr.ttf", + "popup_key_font": "segoepr.ttf", + "key_main_font": "segoepr.ttf", + "key_alt_font": "segoepr.ttf" +} +``` + +空字符串表示使用系统默认字体。 + +## 使用方法 + +### 用户操作 +1. 将字体文件 (`.ttf` 或 `.otf`) 放入应用的 `fonts/` 目录 +2. 打开 Fcitx5 设置 → 字体 +3. 点击要配置的字体项目 +4. 从弹出的列表中选择字体 +5. 配置立即生效 + +### 开发者集成 +```kotlin +// 手动更新字体配置 +FontManager.updateFontConfiguration() + +// 获取可用字体列表 +val fonts = FontManager.getAvailableFonts() + +// 显示字体选择对话框 +FontManager.showFontPickerDialog(context, R.string.font_title) { selectedFont -> + // 处理选择的字体 +} +``` + +## 技术实现亮点 + +1. **模块化设计**: 字体管理功能独立模块,易于维护和扩展 +2. **异常安全**: 完善的错误处理,避免因字体配置问题导致应用崩溃 +3. **性能优化**: 字体缓存机制,避免重复加载 +4. **用户体验**: 实时预览字体名称,直观的选择界面 +5. **向后兼容**: 兼容现有的字体配置文件格式 + +## 国际化支持 + +已添加中英文字符串资源: +- 英文: `values/strings.xml` +- 简体中文: `values-zh-rCN/strings.xml` + +## 测试 + +使用 `FontConfigTest` 类进行功能验证: +```kotlin +// 创建测试配置 +FontConfigTest.createTestFontConfig() + +// 验证配置正确性 +val isValid = FontConfigTest.validateFontConfig() +``` + +## 注意事项 + +1. 字体文件需要放在应用的外部存储 `fonts/` 目录下 +2. 支持的字体格式:`.ttf`, `.otf` +3. 配置更改后会自动清理字体缓存以确保更改生效 +4. 空字体文件名表示使用系统默认字体 + +## 未来扩展 + +- 字体预览功能 +- 字体大小配置 +- 字体样式配置(粗体、斜体等) +- 字体文件在线下载功能 diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt new file mode 100644 index 000000000..7114ff722 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontConfigTest.kt @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.data.fonts + +import org.fcitx.fcitx5.android.utils.appContext +import org.json.JSONObject +import java.io.File + +/** + * 字体配置测试工具类 + */ +object FontConfigTest { + + /** + * 创建一个测试字体配置 + */ + fun createTestFontConfig() { + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + if (!fontsDir.exists()) { + fontsDir.mkdirs() + } + + val configFile = File(fontsDir, "fontset.json") + val testConfig = JSONObject().apply { + put("cand_font", "") // 使用系统默认 + put("font", "") // 使用系统默认 + put("preedit_font", "") + put("popup_key_font", "") + put("key_main_font", "") + put("key_alt_font", "") + } + + configFile.writeText(testConfig.toString(2)) + println("创建测试字体配置: ${configFile.absolutePath}") + } + + /** + * 验证字体配置是否正确加载 + */ + fun validateFontConfig(): Boolean { + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + val configFile = File(fontsDir, "fontset.json") + + return try { + if (!configFile.exists()) { + println("字体配置文件不存在") + return false + } + + val config = JSONObject(configFile.readText()) + val requiredKeys = listOf("cand_font", "font", "preedit_font", "popup_key_font", "key_main_font", "key_alt_font") + + requiredKeys.all { key -> + config.has(key).also { hasKey -> + if (!hasKey) println("缺少必需的键: $key") + } + } + } catch (e: Exception) { + println("验证字体配置时出错: ${e.message}") + false + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt new file mode 100644 index 000000000..e2f6d9886 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/fonts/FontManager.kt @@ -0,0 +1,179 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.data.fonts + +import android.app.AlertDialog +import android.content.Context +import android.util.Log +import androidx.annotation.StringRes +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.prefs.AppPrefs +import org.fcitx.fcitx5.android.input.AutoScaleTextView +import org.fcitx.fcitx5.android.utils.appContext +import org.json.JSONObject +import java.io.File + +object FontManager { + + private const val TAG = "FontManager" + + private val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + private val fontConfigFile = File(fontsDir, "fontset.json") + + // 字体更改监听器 + interface OnFontChangeListener { + fun onFontConfigurationChanged() + } + + private val fontChangeListeners = mutableListOf() + + init { + if (!fontsDir.exists()) { + fontsDir.mkdirs() + } + } + + /** + * 添加字体更改监听器 + */ + fun addOnFontChangeListener(listener: OnFontChangeListener) { + fontChangeListeners.add(listener) + } + + /** + * 移除字体更改监听器 + */ + fun removeOnFontChangeListener(listener: OnFontChangeListener) { + fontChangeListeners.remove(listener) + } + + /** + * 通知所有监听器字体配置已更改 + */ + private fun notifyFontConfigurationChanged() { + Log.d(TAG, "通知字体配置更改,监听器数量: ${fontChangeListeners.size}") + fontChangeListeners.forEach { listener -> + try { + listener.onFontConfigurationChanged() + } catch (e: Exception) { + Log.e(TAG, "字体更改监听器执行失败", e) + } + } + } + + /** + * 获取可用字体列表 + */ + fun getAvailableFonts(): List { + val fonts = mutableListOf() + + // 添加系统默认字体 + fonts.add(FontInfo("", "System Default")) + + // 扫描 fonts 目录 + if (fontsDir.exists()) { + fontsDir.listFiles { file -> + file.isFile && (file.extension.lowercase() in listOf("ttf", "otf")) + }?.forEach { file -> + fonts.add(FontInfo(file.name, file.nameWithoutExtension)) + } + } + + return fonts.sortedBy { it.displayName } + } + + /** + * 显示字体选择对话框 + */ + fun showFontPickerDialog( + context: Context, + @StringRes titleRes: Int, + onFontSelected: (String) -> Unit + ) { + val fonts = getAvailableFonts() + val fontNames = fonts.map { it.displayName }.toTypedArray() + + AlertDialog.Builder(context) + .setTitle(titleRes) + .setItems(fontNames) { _, which -> + val selectedFont = fonts[which].fileName + onFontSelected(selectedFont) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + /** + * 更新字体配置文件 + */ + fun updateFontConfiguration() { + try { + Log.d(TAG, "开始更新字体配置") + + val fontPrefs = AppPrefs.getInstance().fonts + val config = JSONObject().apply { + val candFont = fontPrefs.candFont.getValue() + val preeditFont = fontPrefs.preeditFont.getValue() + val popupKeyFont = fontPrefs.popupKeyFont.getValue() + val keyMainFont = fontPrefs.keyMainFont.getValue() + val keyAltFont = fontPrefs.keyAltFont.getValue() + val defaultFont = fontPrefs.defaultFont.getValue() + + Log.d(TAG, "读取到的字体配置:") + Log.d(TAG, " - candFont: '$candFont'") + Log.d(TAG, " - preeditFont: '$preeditFont'") + Log.d(TAG, " - popupKeyFont: '$popupKeyFont'") + Log.d(TAG, " - keyMainFont: '$keyMainFont'") + Log.d(TAG, " - keyAltFont: '$keyAltFont'") + Log.d(TAG, " - defaultFont: '$defaultFont'") + + put("cand_font", candFont) + put("font", defaultFont) + put("preedit_font", preeditFont) + put("popup_key_font", popupKeyFont) + put("key_main_font", keyMainFont) + put("key_alt_font", keyAltFont) + } + + Log.d(TAG, "目标目录: ${fontsDir.absolutePath}") + Log.d(TAG, "目标文件: ${fontConfigFile.absolutePath}") + + if (!fontsDir.exists()) { + Log.d(TAG, "创建字体目录") + val created = fontsDir.mkdirs() + Log.d(TAG, "目录创建结果: $created") + } + + Log.d(TAG, "目录是否存在: ${fontsDir.exists()}") + Log.d(TAG, "目录是否可写: ${fontsDir.canWrite()}") + + fontConfigFile.writeText(config.toString(2)) + + Log.d(TAG, "文件是否存在: ${fontConfigFile.exists()}") + Log.d(TAG, "文件大小: ${if (fontConfigFile.exists()) fontConfigFile.length() else "N/A"}") + + // 添加日志信息 + Log.i(TAG, "字体配置已更新到文件: ${fontConfigFile.absolutePath}") + Log.d(TAG, "配置内容: ${config.toString(2)}") + + // 清除字体缓存,触发重新加载 + AutoScaleTextView.clearFontCache() + + // 通知所有监听器字体配置已更改 + notifyFontConfigurationChanged() + } catch (e: Exception) { + // 记录错误但不崩溃 + Log.e(TAG, "更新字体配置时出错: ${e.message}", e) + } + } + + /** + * 字体信息 + */ + data class FontInfo( + val fileName: String, + val displayName: String + ) +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 6a4248401..ab592c1a7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -6,6 +6,7 @@ package org.fcitx.fcitx5.android.data.prefs import android.content.SharedPreferences import android.os.Build +import android.util.Log import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.core.content.edit @@ -359,6 +360,36 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { ) } + inner class Fonts : ManagedPreferenceCategory(R.string.fonts, sharedPreferences) { + val candFont: ManagedPreference.PString + val preeditFont: ManagedPreference.PString + val popupKeyFont: ManagedPreference.PString + val keyMainFont: ManagedPreference.PString + val keyAltFont: ManagedPreference.PString + val defaultFont: ManagedPreference.PString + + init { + candFont = ManagedPreference.PString(sharedPreferences, "cand_font", "").apply { register() } + preeditFont = ManagedPreference.PString(sharedPreferences, "preedit_font", "").apply { register() } + popupKeyFont = ManagedPreference.PString(sharedPreferences, "popup_key_font", "").apply { register() } + keyMainFont = ManagedPreference.PString(sharedPreferences, "key_main_font", "").apply { register() } + keyAltFont = ManagedPreference.PString(sharedPreferences, "key_alt_font", "").apply { register() } + defaultFont = ManagedPreference.PString(sharedPreferences, "font", "").apply { register() } + + val onChange = ManagedPreference.OnChangeListener { key, value -> + Log.d("AppPrefs.Fonts", "字体配置更改 - $key: $value") + org.fcitx.fcitx5.android.data.fonts.FontManager.updateFontConfiguration() + } + + candFont.registerOnChangeListener(onChange) + preeditFont.registerOnChangeListener(onChange) + popupKeyFont.registerOnChangeListener(onChange) + keyMainFont.registerOnChangeListener(onChange) + keyAltFont.registerOnChangeListener(onChange) + defaultFont.registerOnChangeListener(onChange) + } + } + private val providers = mutableListOf() fun registerProvider( @@ -378,6 +409,7 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { val candidates = Candidates().register() val clipboard = Clipboard().register() val symbols = Symbols().register() + val fonts = Fonts().register() val advanced = Advanced().register() @Keep diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt new file mode 100644 index 000000000..c700bc5af --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/FontPrefs.kt @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.data.prefs + +import android.content.SharedPreferences +import androidx.annotation.StringRes +import androidx.preference.PreferenceScreen +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.fonts.FontManager +import org.fcitx.fcitx5.android.utils.addPreference + +class FontPrefs(sharedPreferences: SharedPreferences) : ManagedPreferenceInternal(sharedPreferences) { + + // 字体配置项 + val candFont = string("cand_font", "") + val preeditFont = string("preedit_font", "") + val popupKeyFont = string("popup_key_font", "") + val keyMainFont = string("key_main_font", "") + val keyAltFont = string("key_alt_font", "") + val defaultFont = string("font", "") + + override fun createUi(screen: PreferenceScreen) { + val ctx = screen.context + + screen.addPreference( + R.string.font_candidate, + summary = getFontDisplayName(candFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_candidate, candFont) } + ) + + screen.addPreference( + R.string.font_preedit, + summary = getFontDisplayName(preeditFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_preedit, preeditFont) } + ) + + screen.addPreference( + R.string.font_popup_key, + summary = getFontDisplayName(popupKeyFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_popup_key, popupKeyFont) } + ) + + screen.addPreference( + R.string.font_key_main, + summary = getFontDisplayName(keyMainFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_key_main, keyMainFont) } + ) + + screen.addPreference( + R.string.font_key_alt, + summary = getFontDisplayName(keyAltFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_key_alt, keyAltFont) } + ) + + screen.addPreference( + R.string.font_default, + summary = getFontDisplayName(defaultFont.getValue()), + onClick = { showFontPicker(ctx, R.string.font_default, defaultFont) } + ) + } + + private fun getFontDisplayName(fontFileName: String): String { + return if (fontFileName.isEmpty()) { + "System Default" + } else { + fontFileName.substringBeforeLast('.') + } + } + + private fun showFontPicker( + context: android.content.Context, + @StringRes titleRes: Int, + preference: ManagedPreference.PString + ) { + FontManager.showFontPickerDialog(context, titleRes) { selectedFont -> + preference.setValue(selectedFont) + FontManager.updateFontConfiguration() + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index 37ada23bd..113585eea 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -17,6 +17,10 @@ import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max import kotlin.math.min +import org.json.JSONObject +import java.io.File +import android.graphics.Typeface +import org.fcitx.fcitx5.android.utils.appContext import kotlin.math.roundToInt @SuppressLint("AppCompatCustomView") @@ -54,6 +58,59 @@ class AutoScaleTextView @JvmOverloads constructor( private var textScaleX = 1.0f private var textScaleY = 1.0f + companion object { + private var cachedFontTypefaceMap: MutableMap? = null + private var lastModified = 0L + val fontTypefaceMap: MutableMap + @Synchronized + get() { + val fontsDir = File(appContext.getExternalFilesDir(null), "fonts") + val jsonFile = File(fontsDir, "fontset.json") + if (!jsonFile.exists()) { + cachedFontTypefaceMap = null + return mutableMapOf() // 返回空Map而非null + } + if (cachedFontTypefaceMap == null || lastModified != jsonFile.lastModified()) { + cachedFontTypefaceMap = runCatching { + JSONObject(jsonFile.readText().replace(Regex("//.*?\\n"), "")) + .let { json -> + json.keys().asSequence().associateTo(mutableMapOf()) { key -> + key to runCatching { + val fontFileName = json.getString(key) + if (fontFileName.isEmpty()) { + null // 使用系统默认字体 + } else { + File(fontsDir, fontFileName) + .takeIf { it.exists() } + ?.let { Typeface.createFromFile(it) } + } + }.getOrNull() + } + } as MutableMap // 确保返回可变Map + }.getOrElse { mutableMapOf() } + lastModified = jsonFile.lastModified() + } + return cachedFontTypefaceMap ?: mutableMapOf() + } + + /** + * 清除字体缓存,强制重新加载字体配置 + */ + @Synchronized + fun clearFontCache() { + cachedFontTypefaceMap = null + lastModified = 0L + } + } + + fun setFontTypeFace(key: String) { + setTypeface(fontTypefaceMap[key] ?: Typeface.DEFAULT) + } + + init { + setFontTypeFace("font") + } + override fun setText(charSequence: CharSequence?, bufferType: BufferType) { // setText can be called in super constructor if (!::text.isInitialized || charSequence == null || !text.contentEquals(charSequence)) { 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 b417f5a64..bf9ad00d9 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 @@ -57,6 +57,7 @@ import org.fcitx.fcitx5.android.core.SubtypeManager import org.fcitx.fcitx5.android.daemon.FcitxConnection import org.fcitx.fcitx5.android.daemon.FcitxDaemon import org.fcitx.fcitx5.android.data.InputFeedbacks +import org.fcitx.fcitx5.android.data.fonts.FontManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceProvider @@ -170,6 +171,14 @@ class FcitxInputMethodService : LifecycleInputMethodService() { replaceInputViews(it) } + @Keep + private val fontChangeListener = object : FontManager.OnFontChangeListener { + override fun onFontConfigurationChanged() { + // 字体配置更改时重新创建输入视图以应用新字体 + replaceInputView(ThemeManager.activeTheme) + } + } + /** * Post a fcitx operation to [jobs] to be executed * @@ -201,6 +210,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } prefs.candidates.registerOnChangeListener(recreateCandidatesViewListener) ThemeManager.addOnChangedListener(onThemeChangeListener) + FontManager.addOnFontChangeListener(fontChangeListener) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { postFcitxJob { SubtypeManager.syncWith(enabledIme()) @@ -234,6 +244,11 @@ class FcitxInputMethodService : LifecycleInputMethodService() { // KeyEvent from physical keyboard (or input method engine forwardKey) // use cached event if available cachedKeyEvents.remove(it.timestamp)?.let { keyEvent -> + // 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 + } currentInputConnection?.sendKeyEvent(keyEvent) return@event } @@ -989,6 +1004,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } prefs.candidates.unregisterOnChangeListener(recreateCandidatesViewListener) ThemeManager.removeOnChangedListener(onThemeChangeListener) + FontManager.removeOnFontChangeListener(fontChangeListener) super.onDestroy() // Fcitx might be used in super.onDestroy() FcitxDaemon.disconnect(javaClass.name) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt index 0da95932a..c4354a439 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt @@ -13,7 +13,7 @@ import org.fcitx.fcitx5.android.input.keyboard.KeyAction import org.fcitx.fcitx5.android.input.keyboard.KeyDef @SuppressLint("ViewConstructor") -class NumberRow(ctx: Context, theme: Theme) : BaseKeyboard(ctx, theme, Layout) { +class NumberRow(ctx: Context, theme: Theme) : BaseKeyboard(ctx, theme, ::Layout) { companion object { val Layout = listOf( listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map { digit -> diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt index e927b15fb..506b0ff65 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateItemUi.kt @@ -17,6 +17,10 @@ import splitties.views.dsl.core.matchParent import splitties.views.dsl.core.view import splitties.views.dsl.core.wrapContent import splitties.views.gravityCenter +import org.json.JSONObject +import java.io.File +import android.graphics.Typeface +import org.fcitx.fcitx5.android.utils.appContext class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui { @@ -28,6 +32,10 @@ class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui { setTextColor(theme.candidateTextColor) } + init { + text.setFontTypeFace("cand_font") + } + override val root = view(::CustomGestureView) { background = pressHighlightDrawable(theme.keyPressHighlightColor) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt index a4151e09f..f1655b9e6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt @@ -31,7 +31,7 @@ import splitties.views.imageResource @SuppressLint("ViewConstructor") class ExpandedCandidateLayout(context: Context, theme: Theme) : ConstraintLayout(context) { - class Keyboard(context: Context, theme: Theme) : BaseKeyboard(context, theme, Layout) { + class Keyboard(context: Context, theme: Theme) : BaseKeyboard(context, theme, ::Layout) { companion object { const val UpBtnLabel = "U" const val DownBtnLabel = "D" @@ -111,4 +111,4 @@ class ExpandedCandidateLayout(context: Context, theme: Theme) : ConstraintLayout fun resetPosition() { recyclerView.scrollToPosition(0) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt index 84a21447c..973a9774e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt @@ -46,9 +46,11 @@ import kotlin.math.roundToInt abstract class BaseKeyboard( context: Context, protected val theme: Theme, - private val keyLayout: List> + private val layoutProvider: () ->List> ) : ConstraintLayout(context) { + private val keyLayout: List> + get() = layoutProvider() var keyActionListener: KeyActionListener? = null private val prefs = AppPrefs.getInstance() @@ -503,4 +505,4 @@ abstract class BaseKeyboard( // do nothing by default } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt index 211a2b37f..d0e9ecc53 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt @@ -48,6 +48,7 @@ open class KeyDef( class AltText( displayText: String, val altText: String, + val character: String, textSize: Float, /** * `Int` constants in [Typeface]. @@ -123,4 +124,4 @@ open class KeyDef( class Item(val label: String, @DrawableRes val icon: Int, val action: KeyAction) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt index 96b1ba94b..e95b4c955 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt @@ -42,12 +42,14 @@ class SymbolKey( class AlphabetKey( val character: String, val punctuation: String, + val displayText: String = character, variant: Variant = Variant.Normal, popup: Array? = null ) : KeyDef( Appearance.AltText( - displayText = character, + displayText = displayText, altText = punctuation, + character = character, textSize = 23f, variant = variant ), @@ -70,6 +72,7 @@ class AlphabetDigitKey( Appearance.AltText( displayText = character, altText = altText, + character = character, textSize = 23f ), setOf( 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 2252d7459..cc1002ed3 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 @@ -244,6 +244,7 @@ open class TextKeyView(ctx: Context, theme: Theme, def: KeyDef.Appearance.Text) setTextSize(TypedValue.COMPLEX_UNIT_DIP, def.textSize) textDirection = View.TEXT_DIRECTION_FIRST_STRONG_LTR // keep original typeface, apply textStyle only + setFontTypeFace("key_main_font") setTypeface(typeface, def.textStyle) setTextColor( when (def.variant) { @@ -271,6 +272,7 @@ class AltTextKeyView(ctx: Context, theme: Theme, def: KeyDef.Appearance.AltText) isFocusable = false // TODO hardcoded alt text size setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10.666667f) + setFontTypeFace("key_alt_font") setTypeface(typeface, Typeface.BOLD) text = def.altText textDirection = View.TEXT_DIRECTION_FIRST_STRONG_LTR diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt index 89dff000a..6c20335a0 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt @@ -16,7 +16,7 @@ import splitties.views.imageResource class NumberKeyboard( context: Context, theme: Theme, -) : BaseKeyboard(context, theme, Layout) { +) : BaseKeyboard(context, theme, ::Layout) { companion object { const val Name = "Number" @@ -68,4 +68,4 @@ class NumberKeyboard( // leave empty on purpose to disable popup in NumberKeyboard } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index f3476aafb..ecaa912b8 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -18,17 +18,85 @@ import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.popup.PopupAction import splitties.views.imageResource +import java.io.File +import kotlinx.serialization.json.* +import org.fcitx.fcitx5.android.utils.appContext +import kotlinx.serialization.Serializable + +object DisplayTextResolver { + fun resolve( + displayText: JsonElement?, + subModeLabel: String, + default: String + ): String { + return when { + displayText == null -> default + displayText is JsonPrimitive -> displayText.content + displayText is JsonObject -> resolveMap(displayText, subModeLabel) ?: default + else -> default + } + } + + private fun resolveMap( + map: JsonObject, + subModeLabel: String + ): String? { + // 直接匹配子模式标签 + return map[subModeLabel]?.jsonPrimitive?.content + ?: map[""]?.jsonPrimitive?.content + } +} @SuppressLint("ViewConstructor") class TextKeyboard( context: Context, theme: Theme -) : BaseKeyboard(context, theme, Layout) { +) : BaseKeyboard(context, theme, ::Layout) { enum class CapsState { None, Once, Lock } companion object { const val Name = "Text" + private var lastModified = 0L + var ime: InputMethodEntry? = null + + @Serializable + data class KeyJson( + val type: String, + val main: String? = null, + val alt: String? = null, + val displayText: JsonElement? = null, + val label: String? = null, + val subLabel: String? = null, + val weight: Float? = null + ) + var cachedLayoutJsonMap: Map>>? = null + + val textLayoutJsonMap: Map>>? + @Synchronized + get() { + val file = File(appContext.getExternalFilesDir(null), "config/TextKeyboardLayout.json") + if (!file.exists()) { + cachedLayoutJsonMap = null + return null + } + if (cachedLayoutJsonMap == null || file.lastModified() != lastModified) { + try { + lastModified = file.lastModified() + val json = file.readText() + cachedLayoutJsonMap = Json.decodeFromString>>>(json) + } catch (e: Exception) { + e.printStackTrace() + cachedLayoutJsonMap = null + } + } + return cachedLayoutJsonMap + } + + private fun getTextLayoutJsonForIme(displayName: String): List>? { + val map = textLayoutJsonMap ?: return null + return map[displayName] ?: null + } val Layout: List> = listOf( listOf( @@ -93,8 +161,6 @@ class TextKeyboard( private val keepLettersUppercase by AppPrefs.getInstance().keyboard.keepLettersUppercase init { - updateLangSwitchKey(showLangSwitchKey.getValue()) - showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener) } private val textKeys: List by lazy { @@ -155,6 +221,12 @@ class TextKeyboard( updateAlphabetKeys() } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + updateLangSwitchKey(showLangSwitchKey.getValue()) + showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener) + } + override fun onReturnDrawableUpdate(returnDrawable: Int) { `return`.img.imageResource = returnDrawable } @@ -165,6 +237,9 @@ class TextKeyboard( } override fun onInputMethodUpdate(ime: InputMethodEntry) { + // update ime of companion object ime + TextKeyboard.ime = ime + updateAlphabetKeys() space.mainText.text = buildString { append(ime.displayName) ime.subMode.run { label.ifEmpty { name.ifEmpty { null } } }?.let { append(" ($it)") } @@ -227,11 +302,40 @@ class TextKeyboard( } private fun updateAlphabetKeys() { - textKeys.forEach { - if (it.def !is KeyDef.Appearance.AltText) return - it.mainText.text = it.def.displayText.let { str -> - if (str.length != 1 || !str[0].isLetter()) return@forEach - if (keepLettersUppercase) str.uppercase() else transformAlphabet(str) + val layoutJson = getTextLayoutJsonForIme(ime?.uniqueName ?: "default") + if (layoutJson != null) { + textKeys.forEach { + if (it.def !is KeyDef.Appearance.AltText) return@forEach + val keyJson = layoutJson.flatten().find { key -> key.main == it.def.character } + val displayText = if (keyJson != null ) { + DisplayTextResolver.resolve( + keyJson.displayText, + ime?.subMode?.label ?: "", + keyJson.main ?: "" + ) + } else { + it.def.character + } + // val displayText = keyJson?.displayText ?: keyJson?.main ?: it.def.character + + it.mainText.text = displayText?.let { str -> + if (keepLettersUppercase) { + keyJson?.main?.uppercase() ?: str.uppercase() + } else { + when(capsState) { + CapsState.None -> displayText.lowercase() ?: keyJson?.main?.lowercase() ?: str.lowercase() + else -> keyJson?.main?.uppercase() ?: str.uppercase() + } + } + } ?: it.def.character + } + } else { + textKeys.forEach { + if (it.def !is KeyDef.Appearance.AltText) return + it.mainText.text = it.def.displayText.let { str -> + if (str.length != 1 || !str[0].isLetter()) return@forEach + if (keepLettersUppercase) str.uppercase() else transformAlphabet(str) + } } } } @@ -251,4 +355,4 @@ class TextKeyboard( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt index 248fbb13b..0998bfb24 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt @@ -28,7 +28,7 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : ConstraintLayout(context) { class Keyboard(context: Context, theme: Theme, switchKey: KeyDef) : BaseKeyboard( - context, theme, listOf( + context, theme, {listOf( listOf( LayoutSwitchKey("ABC", TextKeyboard.Name), PunctuationKey(","), @@ -37,7 +37,7 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : PunctuationKey("."), ReturnKey() ) - ) + )} ) { class PunctuationKey(val symbol: String) : KeyDef( @@ -84,4 +84,4 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : below(pager, dp(-1)) }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt index 1d8ffe161..45b6b6e9e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt @@ -27,6 +27,9 @@ import splitties.views.dsl.core.add import splitties.views.dsl.core.frameLayout import splitties.views.dsl.core.lParams import java.util.LinkedList +import kotlinx.serialization.json.Json +import org.fcitx.fcitx5.android.utils.appContext +import java.io.File class PopupComponent : UniqueComponent(), Dependent, ManagedHandler by managedHandler() { @@ -62,6 +65,32 @@ class PopupComponent : private val rootLocation = intArrayOf(0, 0) private val rootBounds: Rect = Rect() + companion object { + private var lastModified = 0L + private var cachedPopupPreset: Map>? = null + val popupPresetJson: Map>? + @Synchronized + get() { + var file = File(appContext.getExternalFilesDir(null), "config/PopupPreset.json") + if (!file.exists()) { + cachedPopupPreset = null + return null + } + if (cachedPopupPreset == null || file.lastModified() != lastModified) { + try { + lastModified = file.lastModified() + val json = file.readText() + cachedPopupPreset = Json.decodeFromString>>(json) + .mapValues { it.value.toTypedArray() } + } catch (e: Exception) { + e.printStackTrace() + cachedPopupPreset = null + } + } + return cachedPopupPreset + } + } + val root by lazy { context.frameLayout { // we want (0, 0) at top left @@ -107,7 +136,8 @@ class PopupComponent : } private fun showKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { - val keys = PopupPreset[keyboard.label] + val keys = popupPresetJson?.get(keyboard.label) + ?: PopupPreset[keyboard.label] ?: EmojiModifier.produceSkinTones(keyboard.label) ?: return // clear popup preview text OR create empty popup preview @@ -130,6 +160,7 @@ class PopupComponent : popupKeyHeight, // position popup keyboard higher, because of [^1] popupHeight + keyBottomMargin, + this.service, keys, labels ) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt index f7ba9c4d3..553d878c1 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupKeyboardUi.kt @@ -7,9 +7,13 @@ package org.fcitx.fcitx5.android.input.popup import android.content.Context import android.graphics.Rect import android.graphics.drawable.GradientDrawable +import android.view.KeyEvent // Added import import android.view.ViewOutlineProvider +import org.fcitx.fcitx5.android.core.KeyState +import org.fcitx.fcitx5.android.core.KeyStates import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.AutoScaleTextView +import org.fcitx.fcitx5.android.input.FcitxInputMethodService // Added import import org.fcitx.fcitx5.android.input.keyboard.KeyAction import splitties.dimensions.dp import splitties.views.dsl.core.Ui @@ -39,6 +43,7 @@ import kotlin.math.roundToInt * @param keyHeight key height in popup keyboard * @param popupHeight popup preview view height. Used to transform gesture coordinate from * trigger view to popup keyboard view. See [offsetX] and [offsetY]. + * @param service The FcitxInputMethodService instance * @param keys character to commit when triggered * @param labels symbols to show on keys */ @@ -52,6 +57,7 @@ class PopupKeyboardUi( private val keyWidth: Int, private val keyHeight: Int, private val popupHeight: Int, + private val service: FcitxInputMethodService, // Added service parameter private val keys: Array, private val labels: Array ) : PopupContainerUi(ctx, theme, outerBounds, triggerBounds, onDismissSelf) { @@ -63,6 +69,7 @@ class PopupKeyboardUi( scaleMode = AutoScaleTextView.Mode.Proportional textSize = 23f setTextColor(theme.keyTextColor) + setFontTypeFace("popup_key_font") } override val root = frameLayout { @@ -218,8 +225,50 @@ class PopupKeyboardUi( } override fun onTrigger(): KeyAction? { - val key = keys.getOrNull(focusedIndex) ?: return null - return KeyAction.FcitxKeyAction(key) - } + val rawKeyString = keys.getOrNull(focusedIndex) ?: return null + + if (rawKeyString.startsWith("Ctrl+") && rawKeyString.length > 5) { + val actualCharStr = rawKeyString.substring(5) + if (actualCharStr.isNotEmpty()) { + val charToPress = actualCharStr[0] + var keyCode = 0 + val upperChar = charToPress.uppercaseChar() + + // Simplified conversion: A-Z, 0-9 + if (upperChar in 'A'..'Z') { + keyCode = KeyEvent.KEYCODE_A + (upperChar.code - 'A'.code) + } else if (charToPress in '0'..'9') { + keyCode = KeyEvent.KEYCODE_0 + (charToPress.code - '0'.code) + } else { + // For other characters, this mapping is incomplete. + // You might want to add specific mappings for common symbols: + // when (charToPress) { + // '.' -> keyCode = KeyEvent.KEYCODE_PERIOD + // ',' -> keyCode = KeyEvent.KEYCODE_COMMA + // // ... etc. + // } + // android.util.Log.w("PopupKeyboardUi", "Cannot map char '$charToPress' to KeyEvent.KEYCODE for Ctrl operation.") + } + if (keyCode != 0) { + service.sendCombinationKeyEvents(keyCode, ctrl = true) + return null // Action handled directly by service, no further KeyAction needed + } else { + // Fallback: If keyCode couldn't be determined for sendCombinationKeyEvents, + // send the original FcitxKeyAction with Ctrl state. + // This ensures some Ctrl functionality remains if mapping fails. + return KeyAction.FcitxKeyAction( + act = actualCharStr, + states = KeyStates(KeyState.Ctrl, KeyState.Virtual) + ) + } + } else { + // "Ctrl+" but no character after it - unlikely, but handle defensively by sending as is. + return KeyAction.FcitxKeyAction(rawKeyString) + } + } else { + // Not a "Ctrl+" key, send as a normal character. + return KeyAction.FcitxKeyAction(rawKeyString) + } + } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt index 11905d2f1..47baa33f5 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupPreset.kt @@ -12,61 +12,61 @@ val PopupPreset: Map> = hashMapOf( // // Latin // - "q" to arrayOf("1", "Q"), - "w" to arrayOf("2", "W"), - "e" to arrayOf("3", "E", "ê", "ë", "ē", "é", "ě", "è", "ė", "ę", "ȩ", "ḝ", "ə"), - "r" to arrayOf("4", "R"), - "t" to arrayOf("5", "T"), - "y" to arrayOf("6", "Y", "ÿ", "ұ", "ү", "ӯ", "ў"), - "u" to arrayOf("7", "U", "û", "ü", "ū", "ú", "ǔ", "ù"), - "i" to arrayOf("8", "I", "î", "ï", "ī", "í", "ǐ", "ì", "į", "ı"), - "o" to arrayOf("9", "O", "ô", "ö", "ō", "ó", "ǒ", "ò", "œ", "ø", "õ"), - "p" to arrayOf("0", "P"), - "a" to arrayOf("@", "A", "â", "ä", "ā", "á", "ǎ", "à", "æ", "ã", "å"), - "s" to arrayOf("*", "S", "ß", "ś", "š", "ş"), - "d" to arrayOf("+", "D", "ð"), - "f" to arrayOf("-", "F"), - "g" to arrayOf("=", "G", "ğ"), - "h" to arrayOf("/", "H"), - "j" to arrayOf("#", "J"), - "k" to arrayOf("(", "[", "{", "K"), - "l" to arrayOf(")", "]", "}", "L", "ł"), - "z" to arrayOf("'", "Z", "`", "ž", "ź", "ż"), - "x" to arrayOf(":", "X", "×"), - "c" to arrayOf("\"", "C", "ç", "ć", "č"), - "v" to arrayOf("?", "V", "¿", "ü", "ǖ", "ǘ", "ǚ", "ǜ"), - "b" to arrayOf("!", "B", "¡"), - "n" to arrayOf("~", "N", "ñ", "ń"), - "m" to arrayOf("\\", "M"), + "q" to arrayOf("1", "Q", "Ctrl+q"), + "w" to arrayOf("2", "W", "Ctrl+w"), + "e" to arrayOf("3", "E", "ê", "ë", "ē", "é", "ě", "è", "ė", "ę", "ȩ", "ḝ", "ə", "Ctrl+e"), + "r" to arrayOf("4", "R", "Ctrl+r"), + "t" to arrayOf("5", "T", "Ctrl+t"), + "y" to arrayOf("6", "Y", "ÿ", "ұ", "ү", "ӯ", "ў", "Ctrl+y"), + "u" to arrayOf("7", "U", "û", "ü", "ū", "ú", "ǔ", "ù", "Ctrl+u"), + "i" to arrayOf("8", "I", "î", "ï", "ī", "í", "ǐ", "ì", "į", "ı", "Ctrl+i"), + "o" to arrayOf("9", "O", "ô", "ö", "ō", "ó", "ǒ", "ò", "œ", "ø", "õ", "Ctrl+o"), + "p" to arrayOf("0", "P", "Ctrl+p"), + "a" to arrayOf("@", "A", "â", "ä", "ā", "á", "ǎ", "à", "æ", "ã", "å", "Ctrl+a"), + "s" to arrayOf("*", "S", "ß", "ś", "š", "ş", "Ctrl+s"), + "d" to arrayOf("+", "D", "ð", "Ctrl+d"), + "f" to arrayOf("-", "F", "Ctrl+f"), + "g" to arrayOf("=", "G", "ğ", "Ctrl+g"), + "h" to arrayOf("/", "H", "Ctrl+h"), + "j" to arrayOf("#", "J", "Ctrl+j"), + "k" to arrayOf("(", "[", "{", "K", "Ctrl+k"), + "l" to arrayOf(")", "]", "}", "L", "ł", "Ctrl+l"), + "z" to arrayOf("'", "Z", "`", "ž", "ź", "ż", "Ctrl+z"), + "x" to arrayOf(":", "X", "×", "Ctrl+x"), + "c" to arrayOf("\"", "C", "ç", "ć", "č", "Ctrl+c"), + "v" to arrayOf("?", "V", "¿", "ü", "ǖ", "ǘ", "ǚ", "ǜ", "Ctrl+v"), + "b" to arrayOf("!", "B", "¡", "Ctrl+b"), + "n" to arrayOf("~", "N", "ñ", "ń", "Ctrl+n"), + "m" to arrayOf("\\", "M", "Ctrl+m"), // // Upper case Latin // - "Q" to arrayOf("1", "q"), - "W" to arrayOf("2", "w"), - "E" to arrayOf("3", "e", "Ê", "Ë", "Ē", "É", "È", "Ė", "Ę", "Ȩ", "Ḝ", "Ə"), - "R" to arrayOf("4", "r"), - "T" to arrayOf("5", "t"), - "Y" to arrayOf("6", "y", "Ÿ", "Ұ", "Ү", "Ӯ", "Ў"), - "U" to arrayOf("7", "u", "Û", "Ü", "Ù", "Ú", "Ū"), - "I" to arrayOf("8", "i", "Î", "Ï", "Í", "Ī", "Į", "Ì"), - "O" to arrayOf("9", "o", "Ô", "Ö", "Ò", "Ó", "Œ", "Ø", "Ō", "Õ"), - "P" to arrayOf("0", "p"), - "A" to arrayOf("@", "a", "Â", "Ä", "Ā", "Á", "À", "Æ", "Ã", "Å"), - "S" to arrayOf("*", "s", "ẞ", "Ś", "Š", "Ş"), - "D" to arrayOf("+", "d", "Ð"), - "F" to arrayOf("-", "f"), - "G" to arrayOf("=", "g", "Ğ"), - "H" to arrayOf("/", "h"), - "J" to arrayOf("#", "j"), - "K" to arrayOf("(", "k"), - "L" to arrayOf(")", "l", "Ł"), - "Z" to arrayOf("'", "z", "`", "Ž", "Ź", "Ż"), - "X" to arrayOf(":", "x"), - "C" to arrayOf("\"", "c", "Ç", "Ć", "Č"), - "V" to arrayOf("?", "v"), - "B" to arrayOf("!", "b", "¡"), - "N" to arrayOf("~", "n", "Ñ", "Ń"), - "M" to arrayOf("\\", "m"), + "Q" to arrayOf("1", "q", "Ctrl+Q"), + "W" to arrayOf("2", "w", "Ctrl+W"), + "E" to arrayOf("3", "e", "Ê", "Ë", "Ē", "É", "È", "Ė", "Ę", "Ȩ", "Ḝ", "Ə", "Ctrl+E"), + "R" to arrayOf("4", "r", "Ctrl+R"), + "T" to arrayOf("5", "t", "Ctrl+T"), + "Y" to arrayOf("6", "y", "Ÿ", "Ұ", "Ү", "Ӯ", "Ў", "Ctrl+Y"), + "U" to arrayOf("7", "u", "Û", "Ü", "Ù", "Ú", "Ū", "Ctrl+U"), + "I" to arrayOf("8", "i", "Î", "Ï", "Í", "Ī", "Į", "Ì", "Ctrl+I"), + "O" to arrayOf("9", "o", "Ô", "Ö", "Ò", "Ó", "Œ", "Ø", "Ō", "Õ", "Ctrl+O"), + "P" to arrayOf("0", "p", "Ctrl+P"), + "A" to arrayOf("@", "a", "Â", "Ä", "Ā", "Á", "À", "Æ", "Ã", "Å", "Ctrl+A"), + "S" to arrayOf("*", "s", "ẞ", "Ś", "Š", "Ş", "Ctrl+S"), + "D" to arrayOf("+", "d", "Ð", "Ctrl+D"), + "F" to arrayOf("-", "f", "Ctrl+F"), + "G" to arrayOf("=", "g", "Ğ", "Ctrl+G"), + "H" to arrayOf("/", "h", "Ctrl+H"), + "J" to arrayOf("#", "j", "Ctrl+J"), + "K" to arrayOf("(", "k", "Ctrl+K"), + "L" to arrayOf(")", "l", "Ł", "Ctrl+L"), + "Z" to arrayOf("'", "z", "`", "Ž", "Ź", "Ż", "Ctrl+Z"), + "X" to arrayOf(":", "x", "Ctrl+X"), + "C" to arrayOf("\"", "c", "Ç", "Ć", "Č", "Ctrl+C"), + "V" to arrayOf("?", "v", "Ctrl+V"), + "B" to arrayOf("!", "b", "¡", "Ctrl+B"), + "N" to arrayOf("~", "n", "Ñ", "Ń", "Ctrl+N"), + "M" to arrayOf("\\", "m", "Ctrl+M"), // // Upper case Cyrillic // diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt index 7c7a8b476..dc3b10b6d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt @@ -23,6 +23,7 @@ import splitties.views.dsl.core.add import splitties.views.dsl.core.lParams import splitties.views.dsl.core.textView import splitties.views.dsl.core.verticalLayout +import org.fcitx.fcitx5.android.input.AutoScaleTextView open class PreeditUi( override val ctx: Context, @@ -48,6 +49,7 @@ open class PreeditUi( setTextColor(theme.keyTextColor) textSize = 16f setupTextView?.invoke(this) + typeface = AutoScaleTextView.fontTypefaceMap["preedit_font"] ?: typeface } private val upView = createTextView() diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt index 20334a125..df99b5e97 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainFragment.kt @@ -85,6 +85,11 @@ class MainFragment : PaddingPreferenceFragment() { R.drawable.ic_baseline_emoji_symbols_24, SettingsRoute.Symbol ) + addDestinationPreference( + R.string.fonts, + R.drawable.ic_baseline_text_format_24, + SettingsRoute.Fonts + ) addDestinationPreference( R.string.plugins, R.drawable.ic_baseline_android_24, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt index 0ca9fac65..7481e08c2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/SettingsRoute.kt @@ -28,6 +28,7 @@ import org.fcitx.fcitx5.android.ui.main.settings.addon.AddonListFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.AdvancedSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.CandidatesSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.ClipboardSettingsFragment +import org.fcitx.fcitx5.android.ui.main.settings.behavior.FontsSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.KeyboardSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.behavior.SymbolSettingsFragment import org.fcitx.fcitx5.android.ui.main.settings.global.GlobalConfigFragment @@ -80,6 +81,9 @@ sealed class SettingsRoute : Parcelable { @Serializable data object Symbol : SettingsRoute() + @Serializable + data object Fonts : SettingsRoute() + @Serializable data object Plugin : SettingsRoute() @@ -219,6 +223,9 @@ sealed class SettingsRoute : Parcelable { fragment { label = ctx.getString(R.string.emoji_and_symbols) } + fragment { + label = ctx.getString(R.string.fonts) + } fragment { label = ctx.getString(R.string.plugins) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt new file mode 100644 index 000000000..d4bd730fa --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/behavior/FontsSettingsFragment.kt @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors + */ +package org.fcitx.fcitx5.android.ui.main.settings.behavior + +import android.os.Bundle +import androidx.preference.PreferenceScreen +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.fonts.FontManager +import org.fcitx.fcitx5.android.data.prefs.AppPrefs +import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceFragment +import org.fcitx.fcitx5.android.utils.addPreference + +class FontsSettingsFragment : ManagedPreferenceFragment(AppPrefs.getInstance().fonts) { + + override fun onPreferenceUiCreated(screen: PreferenceScreen) { + super.onPreferenceUiCreated(screen) + val fontPrefs = AppPrefs.getInstance().fonts + + screen.addPreference( + R.string.font_candidate, + summary = getFontDisplayName(fontPrefs.candFont.getValue()), + onClick = { showFontPicker(R.string.font_candidate, fontPrefs.candFont) } + ) + + screen.addPreference( + R.string.font_preedit, + summary = getFontDisplayName(fontPrefs.preeditFont.getValue()), + onClick = { showFontPicker(R.string.font_preedit, fontPrefs.preeditFont) } + ) + + screen.addPreference( + R.string.font_popup_key, + summary = getFontDisplayName(fontPrefs.popupKeyFont.getValue()), + onClick = { showFontPicker(R.string.font_popup_key, fontPrefs.popupKeyFont) } + ) + + screen.addPreference( + R.string.font_key_main, + summary = getFontDisplayName(fontPrefs.keyMainFont.getValue()), + onClick = { showFontPicker(R.string.font_key_main, fontPrefs.keyMainFont) } + ) + + screen.addPreference( + R.string.font_key_alt, + summary = getFontDisplayName(fontPrefs.keyAltFont.getValue()), + onClick = { showFontPicker(R.string.font_key_alt, fontPrefs.keyAltFont) } + ) + + screen.addPreference( + R.string.font_default, + summary = getFontDisplayName(fontPrefs.defaultFont.getValue()), + onClick = { showFontPicker(R.string.font_default, fontPrefs.defaultFont) } + ) + } + + private fun getFontDisplayName(fontFileName: String): String { + return if (fontFileName.isEmpty()) { + getString(R.string.system_default) + } else { + fontFileName.substringBeforeLast('.') + } + } + + private fun showFontPicker( + titleRes: Int, + preference: org.fcitx.fcitx5.android.data.prefs.ManagedPreference.PString + ) { + FontManager.showFontPickerDialog(requireContext(), titleRes) { selectedFont -> + preference.setValue(selectedFont) + // 手动触发字体配置更新 + FontManager.updateFontConfiguration() + // 刷新设置界面以显示新的摘要 + preferenceScreen.removeAll() + onCreatePreferences(null, null) + } + } +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4f9dfbcd3..10600ea6d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -303,4 +303,21 @@ 把光标移动到结尾 打开输入法设置 全部删除 + 表情和符号 + 默认表情符号肤色修饰符 + 无修饰符 + 🏻 非常浅色 + 🏼 浅色 + 🏽 中等色 + 🏾 深色 + 🏿 非常深色 + + + 字体 + 候选字体 + 编码字体 + 弹出按键字体 + 按键主字体 + 按键次字体 + 默认字体 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index eb8a1dce8..fb474fe6a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -284,4 +284,12 @@ 忽略系統窗口邊襯區 (WindowInsets) 瀏覽用户資料目錄 重複按鍵時的觸覺回饋 - + 表情和符號 + 預設表情符號膚色修飾符 + 無修飾符 + 🏻 非常淺色 + 🏼 淺色 + 🏽 中等色 + 🏾 深色 + 🏿 非常深色 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ab841db5..2f76e9d5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -315,4 +315,13 @@ 🏾 Type-5 🏿 Type-6 Hide unsupported Emojis + + + Fonts + Candidate font + Preedit font + Popup key font + Key main font + Key alt font + Default font diff --git a/prepare_personal_build.sh b/prepare_personal_build.sh new file mode 100755 index 000000000..07c88f63e --- /dev/null +++ b/prepare_personal_build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/bash + +# update fcitx5-rime +echo "updating fcitx5-rime" +pushd plugin/rime/src/main/cpp/fcitx5-rime +git remote add gh https://github.com/fxliang/fcitx5-rime.git || git remote set-url gh https://github.com/fxliang/fcitx5-rime.git +git fetch -v gh master +git checkout gh/master +popd +sed -i 's|/fcitx/|/fxliang/|g' plugin/rime/licenses/libraries/fcitx5-rime.json + +# update prebuilt +echo "updating prebuilt" +pushd lib/fcitx5/src/main/cpp/prebuilt +git remote add gh https://github.com/fxliang/prebuilt.git || git remote set-url gh https://github.com/fxliang/prebuilt.git +git fetch -v gh master +git checkout gh/master +popd