diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml
new file mode 100644
index 000000000..98b5de241
--- /dev/null
+++ b/.github/workflows/build-release-apk.yml
@@ -0,0 +1,64 @@
+name: Build Release APKs
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v3
+
+ - name: Decode Keystore
+ env:
+ RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
+ if: ${{ env.RELEASE_KEYSTORE != '' }}
+ run: |
+ echo "${{ secrets.RELEASE_KEYSTORE }}" | base64 -d > leantype-release.jks
+ echo "keyAlias=${{ secrets.RELEASE_KEY_ALIAS }}" > keystore.properties
+ echo "keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }}" >> keystore.properties
+ echo "storeFile=leantype-release.jks" >> keystore.properties
+ echo "storePassword=${{ secrets.RELEASE_STORE_PASSWORD }}" >> keystore.properties
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build Release APKs
+ run: ./gradlew assembleStandardRelease assembleStandardfullRelease assembleOfflineRelease assembleOfflineliteRelease
+
+ - name: Generate Release Notes
+ # ponytail: generate release notes from changelog during build
+ run: python3 docs/scripts/generate_release_notes.py
+
+ - name: Upload APKs and Release Notes
+ uses: actions/upload-artifact@v4
+ with:
+ name: LeanType-Release-APKs
+ path: |
+ app/build/outputs/apk/**/*.apk
+ docs/releasenote/release_notes_v*.md
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ if: startsWith(github.ref, 'refs/tags/v')
+ with:
+ body_path: docs/releasenote/release_notes_temp.md
+ files: |
+ app/build/outputs/apk/standard/release/*.apk
+ app/build/outputs/apk/standardfull/release/*.apk
+ app/build/outputs/apk/offline/release/*.apk
+ app/build/outputs/apk/offlinelite/release/*.apk
diff --git a/.gitignore b/.gitignore
index 9e18d66b9..4fbd971d2 100755
--- a/.gitignore
+++ b/.gitignore
@@ -82,3 +82,6 @@ docs/superpowers/
# AI agent config (personal, not shared)
.pi/
+
+# Temporary generated files
+docs/releasenote/release_notes_temp.md
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2823bd71e..84eecc658 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,8 +42,8 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
shown. (Upstream bug LeanBitLab/LeanType#186; upstream PR #194.)
### Upstream
-- Merged **LeanBitLab/LeanType v3.8.8** (from v3.8.3, including v3.8.7 and two post-tag docs/badge
- commits) — the source of the handwriting, llama.cpp/GGUF, dynamic downloader, touchpad-gesture,
+- Merged **LeanBitLab/LeanType v3.8.9** (from v3.8.3, including v3.8.7/v3.8.8 and one post-tag docs/badge
+ commit) — the source of the handwriting, llama.cpp/GGUF, dynamic downloader, text-editing mode, touchpad-gesture,
SMS-OTP, selective-backup, and dictionary-downloader changes above. Fork identity (LeanTypeDual,
distinct `applicationId`, two-thumb typing, the Gemini standard-AI layer, and the privacy tiers) is
preserved.
diff --git a/README.md b/README.md
index 095bd0754..828457305 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Type with **both thumbs gliding at the same time**: LeanTypeDual aggregates mult
- **🪟 Floating Keyboard** - Detach the keyboard into a draggable, resizable window (true OS-level overlay), with an optional persistent mode.
- **⌨️ Dual Toolbar / Split Suggestions** - Split the suggestion strip and toolbar for easier reach.
- **🖱️ Touchpad Mode** - Swipe the spacebar up for a cursor touchpad with sensitivity controls and edge-scroll acceleration, including a full-screen laptop-style mode.
+- **✍️ Text editing mode** - A toolbar key opens a text-editing overlay for selection, cursor movement, and clipboard actions.
- **🎨 Modern UI** - "Squircle" key backgrounds, refined icons, and polished aesthetics.
- **🔄 Google Dictionary Import** - Import your personal dictionary words.
- **🔍 Clipboard Search & Undo** - Search clipboard history from the toolbar, undo accidental deletions, and fold pinned items by default.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f16260ef6..39f3bf9eb 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -28,7 +28,7 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
ndk {
- abiFilters.addAll(arrayOf("arm64-v8a"))
+ abiFilters.addAll(arrayOf("armeabi-v7a", "arm64-v8a"))
}
}
@@ -38,6 +38,10 @@ android {
dimension = "privacy"
minSdk = 23
}
+ create("standardfull") {
+ dimension = "privacy"
+ minSdk = 23
+ }
create("offline") {
dimension = "privacy"
applicationIdSuffix = ".offline"
@@ -102,6 +106,7 @@ android {
val flavor = productFlavors.firstOrNull()?.name ?: ""
val number = when(flavor) {
"standard" -> "1"
+ "standardfull" -> "1"
"offline" -> "2"
"offlinelite" -> "3"
else -> ""
@@ -124,7 +129,7 @@ android {
variant.proguardFiles.add(project.layout.buildDirectory.file(getDefaultProguardFile("proguard-android.txt").absolutePath))
variant.proguardFiles.add(project.layout.buildDirectory.file(project.buildFile.parent + "/proguard-rules.pro"))
}
- if (variant.flavorName == "standard") {
+ if (variant.flavorName == "standard" || variant.flavorName == "standardfull") {
// ponytail: dynamically find all dict files to ignore in standard flavor except main_en-US.dict
val dictsDir = project.file("src/main/assets/dicts")
if (dictsDir.exists() && dictsDir.isDirectory) {
@@ -206,6 +211,12 @@ android {
// these orphaned strings are harmlessly stripped by R8 during minification.
disable += "ExtraTranslation"
}
+
+ sourceSets {
+ getByName("standardfull") {
+ java.srcDirs("src/standard/java")
+ }
+ }
}
dependencies {
@@ -234,6 +245,8 @@ dependencies {
// gemini ai proofreading
"standardImplementation"("com.google.ai.client.generativeai:generativeai:0.9.0")
"standardImplementation"("androidx.security:security-crypto:1.1.0-alpha06") // for encrypted API key storage
+ "standardfullImplementation"("com.google.ai.client.generativeai:generativeai:0.9.0")
+ "standardfullImplementation"("androidx.security:security-crypto:1.1.0-alpha06")
// local llm proofreading (offline)
"offlineImplementation"("io.github.ljcamargo:llamacpp-kotlin:0.4.0")
@@ -249,7 +262,7 @@ dependencies {
// ML Kit Digital Ink Recognition — required by the handwriting plugin.
// ML Kit's internal asset manager and native library loader use the host app context,
// so the host app must compile and include the client library resources/libraries.
- "standardImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")
+ "standardfullImplementation"("com.google.mlkit:digital-ink-recognition:19.0.0")
// test
testImplementation(kotlin("test"))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c32d5edf0..59b51eedb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -139,6 +139,19 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
tools:node="remove" />
+
+
+
+
+
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
index d347b04c0..d6134485a 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
@@ -149,6 +149,9 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
KeyCode.TOGGLE_TOUCHPAD_MODE -> {
PointerTracker.sPersistentTouchpadModeActive = !PointerTracker.sPersistentTouchpadModeActive
if (PointerTracker.sPersistentTouchpadModeActive) {
+ sPersistentTextEditModeActive = false
+ keyboardSwitcher.hideTextEditView()
+
val touchpadView = keyboardSwitcher.touchpadView
if (touchpadView != null) {
setupTouchpadListener(touchpadView)
@@ -159,6 +162,29 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
}
return
}
+ KeyCode.TOGGLE_TEXT_EDIT_MODE -> {
+ sPersistentTextEditModeActive = !sPersistentTextEditModeActive
+ if (sPersistentTextEditModeActive) {
+ PointerTracker.sPersistentTouchpadModeActive = false
+ keyboardSwitcher.hideTouchpadView()
+
+ val textEditView = keyboardSwitcher.textEditView
+ if (textEditView != null) {
+ setupTextEditListener(textEditView)
+ keyboardSwitcher.showTextEditView()
+ }
+ } else {
+ keyboardSwitcher.hideTextEditView()
+ }
+ return
+ }
+ KeyCode.FORWARD_DELETE -> {
+ val connection = inputLogic.connection
+ val eventTime = android.os.SystemClock.uptimeMillis()
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL, 0, 0))
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_FORWARD_DEL, 0, 0))
+ return
+ }
}
val mkv = keyboardSwitcher.mainKeyboardView
@@ -637,10 +663,50 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
override fun onThreeFingerSwipeDown() {
onCodeInput(KeyCode.REDO, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
}
+ override fun onClose() {
+ PointerTracker.sPersistentTouchpadModeActive = false
+ keyboardSwitcher.hideTouchpadView()
+ }
+ })
+ }
+
+ fun setupTextEditListener(textEditView: TextEditView) {
+ textEditView.setTextEditListener(object : TextEditView.TextEditListener {
+ override fun onCursorMove(keyCode: Int, isSelecting: Boolean) {
+ if (isSelecting) {
+ val androidKeyCode = when (keyCode) {
+ KeyCode.ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
+ KeyCode.ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
+ KeyCode.ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
+ KeyCode.ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
+ else -> 0
+ }
+ if (androidKeyCode != 0) {
+ val eventTime = android.os.SystemClock.uptimeMillis()
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, androidKeyCode, 0, KeyEvent.META_SHIFT_ON))
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, androidKeyCode, 0, KeyEvent.META_SHIFT_ON))
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
+ }
+ } else {
+ onCodeInput(keyCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ }
+ }
+
+ override fun onCodeInput(keyCode: Int) {
+ onCodeInput(keyCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ }
+
+ override fun onClose() {
+ sPersistentTextEditModeActive = false
+ keyboardSwitcher.hideTextEditView()
+ }
})
}
companion object {
+ @JvmField
+ var sPersistentTextEditModeActive = false
private enum class MetaPressState {
UNSET, // default state, not active
SET, // enabled without onPressKey (e.g. in popup)
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
index a5490480d..bdb2b7f08 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
@@ -72,6 +72,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private ClipboardHistoryView mClipboardHistoryView;
private HandwritingView mHandwritingView;
private TouchpadView mTouchpadView;
+ private TextEditView mTextEditView;
private TextView mFakeToastView;
private LatinIME mLatinIME;
private RichInputMethodManager mRichImm;
@@ -343,7 +344,7 @@ private void setMainKeyboardFrame(
final int stripVisibility = settingsValues.mToolbarMode == ToolbarMode.HIDDEN ? View.GONE : View.VISIBLE;
mStripContainer.setVisibility(stripVisibility);
PointerTracker.switchTo(mKeyboardView);
- if (PointerTracker.sPersistentTouchpadModeActive) {
+ if (PointerTracker.sPersistentTouchpadModeActive || KeyboardActionListenerImpl.sPersistentTextEditModeActive) {
mKeyboardView.setVisibility(visibility == View.VISIBLE ? View.INVISIBLE : View.GONE);
} else {
mKeyboardView.setVisibility(visibility);
@@ -382,6 +383,21 @@ private void setMainKeyboardFrame(
} else {
if (mTouchpadView != null) mTouchpadView.setVisibility(View.GONE);
}
+
+ if (KeyboardActionListenerImpl.sPersistentTextEditModeActive) {
+ if (mTextEditView != null) {
+ mTextEditView.setVisibility(visibility);
+ mTextEditView.applyColors(Settings.getValues().mColors);
+ mTextEditView.setPadding(
+ mKeyboardView.getPaddingLeft(),
+ mKeyboardView.getPaddingTop(),
+ mKeyboardView.getPaddingRight(),
+ mKeyboardView.getPaddingBottom()
+ );
+ }
+ } else {
+ if (mTextEditView != null) mTextEditView.setVisibility(View.GONE);
+ }
}
// Implements {@link KeyboardState.SwitchActions}.
@@ -394,6 +410,10 @@ public void setEmojiKeyboard() {
if (mTouchpadView != null) {
mTouchpadView.setVisibility(View.GONE);
}
+ KeyboardActionListenerImpl.sPersistentTextEditModeActive = false;
+ if (mTextEditView != null) {
+ mTextEditView.setVisibility(View.GONE);
+ }
mMainKeyboardFrame.setVisibility(View.VISIBLE);
// The visibility of {@link #mKeyboardView} must be aligned with {@link
// #MainKeyboardFrame}.
@@ -422,6 +442,10 @@ public void setClipboardKeyboard() {
if (mTouchpadView != null) {
mTouchpadView.setVisibility(View.GONE);
}
+ KeyboardActionListenerImpl.sPersistentTextEditModeActive = false;
+ if (mTextEditView != null) {
+ mTextEditView.setVisibility(View.GONE);
+ }
mMainKeyboardFrame.setVisibility(View.VISIBLE);
// The visibility of {@link #mKeyboardView} must be aligned with {@link
// #MainKeyboardFrame}.
@@ -449,6 +473,10 @@ public void setHandwritingKeyboard() {
if (mTouchpadView != null) {
mTouchpadView.setVisibility(View.GONE);
}
+ KeyboardActionListenerImpl.sPersistentTextEditModeActive = false;
+ if (mTextEditView != null) {
+ mTextEditView.setVisibility(View.GONE);
+ }
mMainKeyboardFrame.setVisibility(View.VISIBLE);
mKeyboardView.setVisibility(View.GONE);
mEmojiTabStripView.setVisibility(View.GONE);
@@ -632,6 +660,9 @@ public void showTouchpadView() {
mKeyboardViewWrapper.findViewById(R.id.btn_stop_one_handed_mode).setVisibility(View.GONE);
mKeyboardViewWrapper.findViewById(R.id.btn_switch_one_handed_mode).setVisibility(View.GONE);
mKeyboardViewWrapper.findViewById(R.id.btn_resize_one_handed_mode).setVisibility(View.GONE);
+ if (Settings.getValues().mTouchpadFullscreen) {
+ mStripContainer.setVisibility(View.GONE);
+ }
// Apply bottom padding to avoid overlapping the navigation bar
mTouchpadView.setPadding(
mKeyboardView.getPaddingLeft(),
@@ -649,6 +680,7 @@ public void hideTouchpadView() {
mTouchpadView.setVisibility(View.GONE);
mKeyboardView.setVisibility(View.VISIBLE);
mKeyboardView.setAlpha(1.0f);
+ mStripContainer.setVisibility(Settings.getValues().mToolbarMode == ToolbarMode.HIDDEN ? View.GONE : View.VISIBLE);
// Restore one-handed buttons if needed
if (mKeyboardViewWrapper.getOneHandedModeEnabled()) {
mKeyboardViewWrapper.findViewById(R.id.btn_stop_one_handed_mode).setVisibility(View.VISIBLE);
@@ -661,6 +693,41 @@ public TouchpadView getTouchpadView() {
return mTouchpadView;
}
+ public void showTextEditView() {
+ if (mTextEditView == null) return;
+ mKeyboardView.setVisibility(View.INVISIBLE);
+ mEmojiPalettesView.setVisibility(View.GONE);
+ mClipboardHistoryView.setVisibility(View.GONE);
+ mKeyboardViewWrapper.findViewById(R.id.btn_stop_one_handed_mode).setVisibility(View.GONE);
+ mKeyboardViewWrapper.findViewById(R.id.btn_switch_one_handed_mode).setVisibility(View.GONE);
+ mKeyboardViewWrapper.findViewById(R.id.btn_resize_one_handed_mode).setVisibility(View.GONE);
+ mTextEditView.setPadding(
+ mKeyboardView.getPaddingLeft(),
+ mKeyboardView.getPaddingTop(),
+ mKeyboardView.getPaddingRight(),
+ mKeyboardView.getPaddingBottom()
+ );
+ mTextEditView.applyColors(Settings.getValues().mColors);
+ mTextEditView.setVisibility(View.VISIBLE);
+ mMainKeyboardFrame.setVisibility(View.VISIBLE);
+ }
+
+ public void hideTextEditView() {
+ if (mTextEditView == null) return;
+ mTextEditView.setVisibility(View.GONE);
+ mKeyboardView.setVisibility(View.VISIBLE);
+ mKeyboardView.setAlpha(1.0f);
+ if (mKeyboardViewWrapper.getOneHandedModeEnabled()) {
+ mKeyboardViewWrapper.findViewById(R.id.btn_stop_one_handed_mode).setVisibility(View.VISIBLE);
+ mKeyboardViewWrapper.findViewById(R.id.btn_switch_one_handed_mode).setVisibility(View.VISIBLE);
+ mKeyboardViewWrapper.findViewById(R.id.btn_resize_one_handed_mode).setVisibility(View.VISIBLE);
+ }
+ }
+
+ public TextEditView getTextEditView() {
+ return mTextEditView;
+ }
+
public void toggleSplitKeyboardMode() {
final Settings settings = Settings.getInstance();
settings.writeSplitKeyboardEnabled(
@@ -921,6 +988,13 @@ public View onCreateInputView(@NonNull Context displayContext, final boolean isH
}
}
+ mTextEditView = mCurrentInputView.findViewById(R.id.text_edit_view);
+ if (KeyboardActionListenerImpl.sPersistentTextEditModeActive && mTextEditView != null) {
+ if (mLatinIME.mKeyboardActionListener instanceof KeyboardActionListenerImpl) {
+ ((KeyboardActionListenerImpl) mLatinIME.mKeyboardActionListener).setupTextEditListener(mTextEditView);
+ }
+ }
+
mKeyboardView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (mTouchpadView != null && mTouchpadView.getVisibility() == View.VISIBLE) {
mTouchpadView.setPadding(
@@ -930,6 +1004,14 @@ public View onCreateInputView(@NonNull Context displayContext, final boolean isH
mKeyboardView.getPaddingBottom()
);
}
+ if (mTextEditView != null && mTextEditView.getVisibility() == View.VISIBLE) {
+ mTextEditView.setPadding(
+ mKeyboardView.getPaddingLeft(),
+ mKeyboardView.getPaddingTop(),
+ mKeyboardView.getPaddingRight(),
+ mKeyboardView.getPaddingBottom()
+ );
+ }
});
return mCurrentInputView;
diff --git a/app/src/main/java/helium314/keyboard/keyboard/TextEditView.java b/app/src/main/java/helium314/keyboard/keyboard/TextEditView.java
new file mode 100644
index 000000000..7005f41a4
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/TextEditView.java
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.keyboard;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
+import helium314.keyboard.latin.R;
+import helium314.keyboard.latin.common.ColorType;
+import helium314.keyboard.latin.common.Colors;
+import helium314.keyboard.latin.settings.Settings;
+
+public class TextEditView extends LinearLayout {
+
+ public interface TextEditListener {
+ void onCursorMove(int keyCode, boolean isSelecting);
+ void onCodeInput(int keyCode);
+ void onClose();
+ }
+
+ private TextEditListener mListener;
+ private boolean mSelectionMode = false;
+
+ // Buttons
+ private TextView mBtnSelectAll;
+ private TextView mBtnSelect;
+ private TextView mBtnCut;
+ private TextView mBtnCopy;
+ private TextView mBtnPaste;
+ private ImageView mBtnClose;
+
+ private ImageView mBtnHome;
+ private ImageView mBtnWordLeft;
+ private ImageView mBtnArrowUp;
+ private ImageView mBtnWordRight;
+ private ImageView mBtnEnd;
+
+ private ImageView mBtnBackspace;
+ private ImageView mBtnArrowLeft;
+ private ImageView mBtnArrowDown;
+ private ImageView mBtnArrowRight;
+ private ImageView mBtnDelete;
+
+ public TextEditView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public TextEditView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public TextEditView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setOrientation(VERTICAL);
+ setClickable(true);
+ setFocusable(true);
+ setFitsSystemWindows(true);
+
+ LayoutInflater.from(context).inflate(R.layout.text_edit_view, this, true);
+
+ mBtnSelectAll = findViewById(R.id.btn_select_all);
+ mBtnSelect = findViewById(R.id.btn_select);
+ mBtnCut = findViewById(R.id.btn_cut);
+ mBtnCopy = findViewById(R.id.btn_copy);
+ mBtnPaste = findViewById(R.id.btn_paste);
+ mBtnClose = findViewById(R.id.btn_close);
+
+ mBtnHome = findViewById(R.id.btn_home);
+ mBtnWordLeft = findViewById(R.id.btn_word_left);
+ mBtnArrowUp = findViewById(R.id.btn_arrow_up);
+ mBtnWordRight = findViewById(R.id.btn_word_right);
+ mBtnEnd = findViewById(R.id.btn_end);
+
+ mBtnBackspace = findViewById(R.id.btn_backspace);
+ mBtnArrowLeft = findViewById(R.id.btn_arrow_left);
+ mBtnArrowDown = findViewById(R.id.btn_arrow_down);
+ mBtnArrowRight = findViewById(R.id.btn_arrow_right);
+ mBtnDelete = findViewById(R.id.btn_delete);
+
+ setupClickListeners();
+ }
+
+ private void setupClickListeners() {
+ mBtnSelectAll.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.CLIPBOARD_SELECT_ALL);
+ });
+
+ mBtnSelect.setOnClickListener(v -> {
+ mSelectionMode = !mSelectionMode;
+ applyColors(Settings.getValues().mColors);
+ });
+
+ mBtnCut.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.CLIPBOARD_CUT);
+ mSelectionMode = false;
+ applyColors(Settings.getValues().mColors);
+ });
+
+ mBtnCopy.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.CLIPBOARD_COPY);
+ mSelectionMode = false;
+ applyColors(Settings.getValues().mColors);
+ });
+
+ mBtnPaste.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.CLIPBOARD_PASTE);
+ });
+
+ mBtnClose.setOnClickListener(v -> {
+ if (mListener != null) mListener.onClose();
+ });
+
+ mBtnHome.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.MOVE_START_OF_LINE);
+ });
+
+ mBtnWordLeft.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.WORD_LEFT);
+ });
+
+ mBtnArrowUp.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCursorMove(KeyCode.ARROW_UP, mSelectionMode);
+ });
+
+ mBtnWordRight.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.WORD_RIGHT);
+ });
+
+ mBtnEnd.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.MOVE_END_OF_LINE);
+ });
+
+ mBtnBackspace.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.DELETE);
+ });
+
+ mBtnArrowLeft.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCursorMove(KeyCode.ARROW_LEFT, mSelectionMode);
+ });
+
+ mBtnArrowDown.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCursorMove(KeyCode.ARROW_DOWN, mSelectionMode);
+ });
+
+ mBtnArrowRight.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCursorMove(KeyCode.ARROW_RIGHT, mSelectionMode);
+ });
+
+ mBtnDelete.setOnClickListener(v -> {
+ if (mListener != null) mListener.onCodeInput(KeyCode.FORWARD_DELETE);
+ });
+ }
+
+ public void setTextEditListener(TextEditListener listener) {
+ mListener = listener;
+ }
+
+ public void applyColors(Colors colors) {
+ colors.setBackground(this, ColorType.MAIN_BACKGROUND);
+
+ int keyTextColor = colors.get(ColorType.KEY_TEXT);
+ int functionalKeyTextColor = colors.get(ColorType.FUNCTIONAL_KEY_TEXT);
+ int keyIconColor = colors.get(ColorType.KEY_ICON);
+
+ // Apply background and text colors to Action Buttons
+ setKeyStyle(mBtnSelectAll, colors, false, keyTextColor);
+ setKeyStyle(mBtnSelect, colors, mSelectionMode, mSelectionMode ? functionalKeyTextColor : keyTextColor);
+ setKeyStyle(mBtnCut, colors, false, keyTextColor);
+ setKeyStyle(mBtnCopy, colors, false, keyTextColor);
+ setKeyStyle(mBtnPaste, colors, false, keyTextColor);
+
+ // Retrieve theme-aware icons
+ KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
+ KeyboardIconsSet iconsSet = (switcher != null && switcher.getKeyboard() != null) ? switcher.getKeyboard().mIconsSet : null;
+
+ setIconKeyStyle(mBtnClose, iconsSet, "close_history", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnHome, iconsSet, "page_start", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnWordLeft, iconsSet, "word_left", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnArrowUp, iconsSet, "up", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnWordRight, iconsSet, "word_right", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnEnd, iconsSet, "page_end", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnBackspace, iconsSet, "delete_key", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnArrowLeft, iconsSet, "left", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnArrowDown, iconsSet, "down", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnArrowRight, iconsSet, "right", colors, false, keyIconColor);
+ setIconKeyStyle(mBtnDelete, iconsSet, "clear_clipboard", colors, false, keyIconColor);
+ }
+
+ private void setKeyStyle(TextView textView, Colors colors, boolean isHighlighted, int textColor) {
+ textView.setBackground(createKeyBackground(colors, isHighlighted));
+ textView.setTextColor(textColor);
+ }
+
+ private void setIconKeyStyle(ImageView imageView, KeyboardIconsSet iconsSet, String iconName, Colors colors, boolean isHighlighted, int iconColor) {
+ imageView.setBackground(createKeyBackground(colors, isHighlighted));
+ if (iconsSet != null) {
+ Drawable icon = iconsSet.getIconDrawable(iconName);
+ if (icon != null) {
+ Drawable mutated = icon.mutate();
+ mutated.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
+ imageView.setImageDrawable(mutated);
+ }
+ }
+ }
+
+ private Drawable createKeyBackground(Colors colors, boolean isHighlighted) {
+ float density = getContext().getResources().getDisplayMetrics().density;
+ GradientDrawable gd = new GradientDrawable();
+ gd.setShape(GradientDrawable.RECTANGLE);
+ gd.setCornerRadius(6f * density);
+
+ ColorType colorType = isHighlighted ? ColorType.FUNCTIONAL_KEY_BACKGROUND : ColorType.KEY_BACKGROUND;
+ gd.setColor(colors.get(colorType));
+ return gd;
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java b/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java
index 6d955ffeb..5ae3b7ebd 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java
@@ -4,6 +4,8 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.GradientDrawable;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
@@ -11,8 +13,10 @@
import android.view.View;
import android.os.Handler;
import android.os.Looper;
+import android.widget.ImageView;
import android.widget.LinearLayout;
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.ColorType;
@@ -41,10 +45,12 @@ public interface TouchpadListener {
void onThreeFingerSwipeRight();
void onThreeFingerSwipeUp();
void onThreeFingerSwipeDown();
+ void onClose();
}
private TouchpadListener mListener;
private View mTouchpadSurface;
+ private ImageView mBtnClose;
private GestureDetector mGestureDetector;
// State
@@ -164,6 +170,12 @@ private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.touchpad_view, this, true);
mTouchpadSurface = findViewById(R.id.touchpad_surface);
+ mBtnClose = findViewById(R.id.btn_close_touchpad);
+ mBtnClose.setOnClickListener(v -> {
+ if (mListener != null) {
+ mListener.onClose();
+ }
+ });
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
@@ -205,6 +217,30 @@ public void applyColors(Colors colors) {
// Root background
colors.setBackground(this, ColorType.MAIN_BACKGROUND);
applySurfaceColor();
+
+ // Style the close button
+ if (mBtnClose != null) {
+ KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
+ KeyboardIconsSet iconsSet = (switcher != null && switcher.getKeyboard() != null) ? switcher.getKeyboard().mIconsSet : null;
+ int keyIconColor = colors.get(ColorType.KEY_ICON);
+
+ // Set rounded background for the close button
+ float density = getContext().getResources().getDisplayMetrics().density;
+ GradientDrawable gd = new GradientDrawable();
+ gd.setShape(GradientDrawable.RECTANGLE);
+ gd.setCornerRadius(6f * density);
+ gd.setColor(colors.get(ColorType.KEY_BACKGROUND));
+ mBtnClose.setBackground(gd);
+
+ if (iconsSet != null) {
+ Drawable icon = iconsSet.getIconDrawable("close_history");
+ if (icon != null) {
+ Drawable mutated = icon.mutate();
+ mutated.setColorFilter(keyIconColor, PorterDuff.Mode.SRC_IN);
+ mBtnClose.setImageDrawable(mutated);
+ }
+ }
+ }
}
private void applySurfaceColor() {
@@ -401,24 +437,24 @@ private void setupTouchSurface() {
return true;
case MotionEvent.ACTION_POINTER_UP:
- android.util.Log.i("TouchpadView", "ACTION_POINTER_UP: pointerCount=" + pointerCount);
- if (pointerCount == 2) {
- removeCallbacks(mTwoFingerLongPressRunnable);
- if (mIsTwoFingerLongPress) {
- mIsTwoFingerLongPress = false;
- mIsTwoFingerScroll = false;
- mIsTwoFingerTap = false;
- return true;
- }
- if (mIsTwoFingerTap && (System.currentTimeMillis() - mTwoFingerDownTime) < 300) {
- mTwoFingerTapCount++;
- removeCallbacks(mTwoFingerTapRunnable);
- postDelayed(mTwoFingerTapRunnable, 250);
- }
+ android.util.Log.i("TouchpadView", "ACTION_POINTER_UP: pointerCount=" + pointerCount);
+ if (pointerCount == 2) {
+ removeCallbacks(mTwoFingerLongPressRunnable);
+ if (mIsTwoFingerLongPress) {
+ mIsTwoFingerLongPress = false;
mIsTwoFingerScroll = false;
mIsTwoFingerTap = false;
+ return true;
}
- return true;
+ if (mIsTwoFingerTap && (System.currentTimeMillis() - mTwoFingerDownTime) < 300) {
+ mTwoFingerTapCount++;
+ removeCallbacks(mTwoFingerTapRunnable);
+ postDelayed(mTwoFingerTapRunnable, 250);
+ }
+ mIsTwoFingerScroll = false;
+ mIsTwoFingerTap = false;
+ }
+ return true;
}
return true;
});
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
index 6a9770945..b7ed505a8 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
@@ -504,7 +504,7 @@ public void afterTextChanged(Editable s) {
downloadBtn.setTextSize(12); // Keep it small to fit
downloadBtn.setAllCaps(false);
downloadBtn.setOnClickListener(v -> {
- if ("standard".equals(BuildConfig.FLAVOR)) {
+ if ("standard".equals(BuildConfig.FLAVOR) || "standardfull".equals(BuildConfig.FLAVOR)) {
downloadEmojiDictionary();
downloadBtn.setText("Downloading...");
downloadBtn.setEnabled(false);
@@ -1039,7 +1039,7 @@ private void updateSplitToolbarEmojiSuggestions() {
if (sDictionaryFacilitator == null) {
// ponytail: show download button on suggestion strip in split mode if dictionary is missing
stripView.setEmojiDownloadButton(() -> {
- if ("standard".equals(BuildConfig.FLAVOR)) {
+ if ("standard".equals(BuildConfig.FLAVOR) || "standardfull".equals(BuildConfig.FLAVOR)) {
downloadEmojiDictionary();
mIsDownloadingEmojiDict = true;
updateSplitToolbarEmojiSuggestions();
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt
index 0cd860269..ec6027d33 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt
@@ -161,6 +161,7 @@ class KeyboardIconsSet private constructor() {
ToolbarKey.FLOATING -> R.drawable.ic_drag_indicator
ToolbarKey.INCOGNITO -> R.drawable.ic_incognito_final
ToolbarKey.TOUCHPAD -> R.drawable.ic_touchpad
+ ToolbarKey.TEXT_EDIT -> R.drawable.ic_text_edit
ToolbarKey.AUTOCORRECT -> R.drawable.ic_autocorrect
ToolbarKey.AUTOSPACE -> R.drawable.ic_autospace
ToolbarKey.AUTO_CAP -> R.drawable.ic_auto_cap
@@ -244,6 +245,7 @@ class KeyboardIconsSet private constructor() {
ToolbarKey.FLOATING -> R.drawable.ic_drag_indicator
ToolbarKey.INCOGNITO -> R.drawable.ic_incognito_final
ToolbarKey.TOUCHPAD -> R.drawable.ic_touchpad
+ ToolbarKey.TEXT_EDIT -> R.drawable.ic_text_edit
ToolbarKey.AUTOCORRECT -> R.drawable.ic_autocorrect
ToolbarKey.AUTOSPACE -> R.drawable.ic_autospace
ToolbarKey.AUTO_CAP -> R.drawable.ic_auto_cap
@@ -327,6 +329,7 @@ class KeyboardIconsSet private constructor() {
ToolbarKey.FLOATING -> R.drawable.ic_drag_indicator
ToolbarKey.INCOGNITO -> R.drawable.ic_incognito_final
ToolbarKey.TOUCHPAD -> R.drawable.ic_touchpad_rounded
+ ToolbarKey.TEXT_EDIT -> R.drawable.ic_text_edit
ToolbarKey.AUTOCORRECT -> R.drawable.ic_autocorrect_rounded
ToolbarKey.AUTOSPACE -> R.drawable.ic_autospace_rounded
ToolbarKey.AUTO_CAP -> R.drawable.ic_auto_cap_rounded
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt
index ac3227cc9..8c1ef3cfb 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt
@@ -30,9 +30,9 @@ object KeyCode {
const val FN = -5
const val FN_LOCK = -6
const val DELETE = -7
- //const val DELETE_WORD = -8
+ const val DELETE_WORD = -8
const val FORWARD_DELETE = -9
- //const val FORWARD_DELETE_WORD = -10
+ const val FORWARD_DELETE_WORD = -10
const val SHIFT = -11
const val CAPS_LOCK = -13
@@ -101,6 +101,7 @@ object KeyCode {
const val URI_COMPONENT_TLD = -255
const val SETTINGS = -301
+ const val TOGGLE_TEXT_EDIT_MODE = -305
const val CURRENCY_SLOT_1 = -801
const val CURRENCY_SLOT_2 = -802
@@ -212,7 +213,7 @@ object KeyCode {
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
// working
CURRENCY_SLOT_1, CURRENCY_SLOT_2, CURRENCY_SLOT_3, CURRENCY_SLOT_4, CURRENCY_SLOT_5, CURRENCY_SLOT_6,
- VOICE_INPUT, LANGUAGE_SWITCH, SETTINGS, DELETE, ALPHA, SYMBOL, EMOJI, CLIPBOARD, CLIPBOARD_CUT, UNDO,
+ VOICE_INPUT, LANGUAGE_SWITCH, SETTINGS, DELETE, DELETE_WORD, FORWARD_DELETE, FORWARD_DELETE_WORD, ALPHA, SYMBOL, EMOJI, CLIPBOARD, CLIPBOARD_CUT, UNDO,
REDO, ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, CLIPBOARD_COPY, CLIPBOARD_PASTE, CLIPBOARD_SELECT_ALL,
CLIPBOARD_SELECT_WORD, TOGGLE_INCOGNITO_MODE, TOGGLE_AUTOCORRECT, TOGGLE_AUTOSPACE,
TOGGLE_AUTO_CAP, TOGGLE_FORCE_AUTO_CAP, JOIN_NEXT, FORCE_NEXT_SPACE, UNDO_WORD, FORWARD_DELETE, MOVE_START_OF_LINE, MOVE_END_OF_LINE,
@@ -227,7 +228,7 @@ object KeyCode {
TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT, SEND_INTENT_ONE, SEND_INTENT_TWO,
SEND_INTENT_THREE, INLINE_EMOJI_SEARCH_DONE, META_LOCK, PROOFREAD, TRANSLATE, SHOW_TRANSLATE_LANGUAGES,
CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, CUSTOM_AI_4, CUSTOM_AI_5,
- CUSTOM_AI_6, CUSTOM_AI_7, CUSTOM_AI_8, CUSTOM_AI_9, CUSTOM_AI_10, CLIPBOARD_SEARCH, TOGGLE_FLOATING_KEYBOARD, TOGGLE_TOUCHPAD_MODE, HANDWRITING, CLEAR_HANDWRITING
+ CUSTOM_AI_6, CUSTOM_AI_7, CUSTOM_AI_8, CUSTOM_AI_9, CUSTOM_AI_10, CLIPBOARD_SEARCH, TOGGLE_FLOATING_KEYBOARD, TOGGLE_TOUCHPAD_MODE, TOGGLE_TEXT_EDIT_MODE, HANDWRITING, CLEAR_HANDWRITING
-> this
// conversion
diff --git a/app/src/main/java/helium314/keyboard/latin/ClipboardHistoryManager.kt b/app/src/main/java/helium314/keyboard/latin/ClipboardHistoryManager.kt
index f6268a03a..ae15f16f8 100644
--- a/app/src/main/java/helium314/keyboard/latin/ClipboardHistoryManager.kt
+++ b/app/src/main/java/helium314/keyboard/latin/ClipboardHistoryManager.kt
@@ -26,6 +26,7 @@ import android.net.Uri
import android.os.Handler
import android.os.Looper
import kotlin.concurrent.thread
+import helium314.keyboard.latin.utils.prefs
class ClipboardHistoryManager(
private val latinIME: LatinIME
@@ -160,6 +161,8 @@ class ClipboardHistoryManager(
mainHandler.postDelayed({
updateLatestScreenshotCache {
dontShowCurrentSuggestion = false
+ val prefs = latinIME.prefs()
+ prefs.edit().remove("last_dismissed_screenshot_uri").apply()
latinIME.setNeutralSuggestionStrip()
}
}, 1000)
@@ -204,6 +207,8 @@ class ClipboardHistoryManager(
if (latinIME.mSettings.current.mClipboardHistoryEnabled) {
thread { fetchPrimaryClip() }
dontShowCurrentSuggestion = false
+ val prefs = latinIME.prefs()
+ prefs.edit().remove("last_dismissed_clipboard_text").apply()
}
}
@@ -411,6 +416,9 @@ class ClipboardHistoryManager(
if (System.currentTimeMillis() - timeStamp > RECENT_TIME_MILLIS) return null
val content = clipItem.coerceToText(latinIME)
if (TextUtils.isEmpty(content)) return null
+ val prefs = latinIME.prefs()
+ val lastDismissedText = prefs.getString("last_dismissed_clipboard_text", "")
+ if (content.toString() == lastDismissedText) return null
val inputType = editorInfo?.inputType ?: InputType.TYPE_NULL
if (InputTypeUtils.isNumberInputType(inputType) && !content.isValidNumber()) return null
@@ -430,7 +438,11 @@ class ClipboardHistoryManager(
}
val closeButton = binding.clipboardSuggestionClose
closeButton.setImageDrawable(latinIME.mKeyboardSwitcher.keyboard.mIconsSet.getIconDrawable(ToolbarKey.CLOSE_HISTORY.name.lowercase()))
- closeButton.setOnClickListener { removeClipboardSuggestion() }
+ closeButton.setOnClickListener {
+ val prefs = latinIME.prefs()
+ prefs.edit().putString("last_dismissed_clipboard_text", content.toString()).apply()
+ removeClipboardSuggestion()
+ }
val colors = latinIME.mSettings.current.mColors
textView.setTextColor(colors.get(ColorType.KEY_TEXT))
@@ -462,6 +474,10 @@ class ClipboardHistoryManager(
return null
}
+ val prefs = latinIME.prefs()
+ val lastDismissedScreenshotUri = prefs.getString("last_dismissed_screenshot_uri", "")
+ if (screenshotInfo.uri.toString() == lastDismissedScreenshotUri) return null
+
val diff = System.currentTimeMillis() - screenshotInfo.dateAdded
if (diff >= RECENT_SCREENSHOT_TIME_MILLIS) {
cachedScreenshotInfo = null
@@ -517,6 +533,8 @@ class ClipboardHistoryManager(
val closeButton = binding.clipboardSuggestionClose
closeButton.setImageDrawable(latinIME.mKeyboardSwitcher.keyboard.mIconsSet.getIconDrawable(ToolbarKey.CLOSE_HISTORY.name.lowercase()))
closeButton.setOnClickListener {
+ val prefs = latinIME.prefs()
+ prefs.edit().putString("last_dismissed_screenshot_uri", contentUri.toString()).apply()
dontShowCurrentSuggestion = true
lastSuggestedScreenshotUri = contentUri.toString()
removeClipboardSuggestion()
@@ -572,7 +590,7 @@ class ClipboardHistoryManager(
}
companion object {
- const val RECENT_TIME_MILLIS = 3 * 60 * 1000L // 3 minutes (for clipboard suggestions)
- const val RECENT_SCREENSHOT_TIME_MILLIS = 4 * 60 * 1000L // 4 minutes
+ const val RECENT_TIME_MILLIS = 1 * 60 * 1000L // 1 minute (for clipboard suggestions)
+ const val RECENT_SCREENSHOT_TIME_MILLIS = 1 * 60 * 1000L // 1 minute
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
index 589707036..cf6ede855 100644
--- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java
+++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
@@ -1788,6 +1788,30 @@ public void setNeutralSuggestionStrip() {
mSuggestionStripView.setToolbarVisibility(false);
return;
}
+ if (currentSettings.mBigramPredictionEnabled) {
+ mInputLogic.getSuggestedWords(SuggestedWords.INPUT_STYLE_PREDICTION, 0, new Suggest.OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(SuggestedWords suggestedWords) {
+ if (suggestedWords != null && !suggestedWords.isEmpty()) {
+ setSuggestedWords(suggestedWords);
+ if (hasSuggestionStripView()) {
+ if (currentSettings.mAutoShowToolbarOnSelect && mInputLogic.getConnection().hasSelection()) {
+ mSuggestionStripView.setToolbarVisibility(true);
+ } else if (currentSettings.mAutoShowToolbarOnSelect) {
+ mSuggestionStripView.setToolbarVisibility(mSuggestionStripView.isToolbarManuallyOpen());
+ }
+ }
+ } else {
+ setNeutralPunctuationSuggestionStrip(currentSettings);
+ }
+ }
+ });
+ } else {
+ setNeutralPunctuationSuggestionStrip(currentSettings);
+ }
+ }
+
+ private void setNeutralPunctuationSuggestionStrip(final SettingsValues currentSettings) {
final SuggestedWords neutralSuggestions = currentSettings.mSuggestPunctuation
? currentSettings.mSpacingAndPunctuations.mSuggestPuncList
: SuggestedWords.getEmptyInstance();
diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryFactory.kt b/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryFactory.kt
index 38e8c778d..01bc3ddd2 100644
--- a/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryFactory.kt
+++ b/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryFactory.kt
@@ -69,7 +69,8 @@ object DictionaryFactory {
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file)
if (header != null) {
val prefs = context.prefs()
- if (!prefs.getBoolean("pref_dict_enabled_${header.mIdString}", true)) {
+ val mainPrefKey = "pref_dict_enabled_main:${header.mIdString.substringAfter(":")}"
+ if (!prefs.getBoolean(mainPrefKey, true) || !prefs.getBoolean("pref_dict_enabled_${header.mIdString}", true)) {
Log.i("DictionaryFactory", "skipping disabled dictionary ${header.mIdString}")
return
}
diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/UserBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/dictionary/UserBinaryDictionary.java
index 4c6e6c279..d42e180bb 100644
--- a/app/src/main/java/helium314/keyboard/latin/dictionary/UserBinaryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/dictionary/UserBinaryDictionary.java
@@ -16,6 +16,7 @@
import com.android.inputmethod.latin.BinaryDictionary;
+import helium314.keyboard.latin.NgramContext;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
@@ -207,6 +208,28 @@ private void addWordsLocked(final Cursor cursor) {
false /* isPossiblyOffensive */,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
+ // ponytail: split phrase into unigrams and n-grams for next-word prediction
+ final String[] parts = word.split("\\s+");
+ if (parts.length > 1) {
+ for (final String part : parts) {
+ if (part.length() <= MAX_WORD_LENGTH && !part.isEmpty() && !part.equals(word)) {
+ runGCIfRequiredLocked(true /* mindsBlockByGC */);
+ addUnigramLocked(part, adjustedFrequency, null, 0, false, false, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+ }
+ }
+ for (int i = 1; i < parts.length; i++) {
+ final String targetWord = parts[i];
+ if (targetWord.length() <= MAX_WORD_LENGTH && !targetWord.isEmpty()) {
+ final int contextSize = Math.min(i, BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM);
+ final NgramContext.WordInfo[] prevWords = new NgramContext.WordInfo[contextSize];
+ for (int j = 0; j < contextSize; j++) {
+ prevWords[j] = new NgramContext.WordInfo(parts[i - 1 - j]);
+ }
+ runGCIfRequiredLocked(true /* mindsBlockByGC */);
+ addNgramEntryLocked(new NgramContext(prevWords), targetWord, adjustedFrequency, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+ }
+ }
+ }
}
cursor.moveToNext();
}
diff --git a/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingLoader.kt b/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingLoader.kt
index 9731bd67d..9e3ad6d93 100644
--- a/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingLoader.kt
+++ b/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingLoader.kt
@@ -47,7 +47,7 @@ object HandwritingLoader {
recognizer.init(context)
activeRecognizer = recognizer
return recognizer
- } catch (e: Exception) {
+ } catch (e: Throwable) {
Log.e("HandwritingLoader", "Failed to load handwriting plugin", e)
}
return null
@@ -100,7 +100,7 @@ object HandwritingLoader {
context.prefs().edit().putBoolean(PREF_HAS_PLUGIN, true).apply()
activeRecognizer = recognizer
return true
- } catch (e: Exception) {
+ } catch (e: Throwable) {
Log.e("HandwritingLoader", "Failed to import plugin APK", e)
// Cleanup on failure
try {
@@ -125,4 +125,4 @@ object HandwritingLoader {
context.prefs().edit().putBoolean(PREF_HAS_PLUGIN, false).apply()
activeRecognizer = null
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingView.kt b/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingView.kt
index 56331ff57..73a382541 100644
--- a/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingView.kt
@@ -144,7 +144,7 @@ class HandwritingView @JvmOverloads constructor(
button.setTextColor(colors.get(ColorType.KEY_TEXT))
// ponytail: download plugin directly on standard flavor, otherwise go to Settings
- if ("standard" == helium314.keyboard.latin.BuildConfig.FLAVOR) {
+ if ("standardfull" == helium314.keyboard.latin.BuildConfig.FLAVOR) {
button.text = "Download Plugin"
button.setOnClickListener {
downloadPlugin(button)
@@ -461,4 +461,4 @@ class HandwritingView @JvmOverloads constructor(
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
index 3ea61ce4d..46f561678 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
@@ -564,6 +564,15 @@ public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, fin
}
}
+ if (oldSelStart != newSelStart || oldSelEnd != newSelEnd) {
+ if (newSelStart != mLastExpandedCursorPosition) {
+ mLastExpandedText = null;
+ mLastShortcutText = null;
+ mLastExpandedCursorPosition = -1;
+ mLastExpandedCursorOffset = -1;
+ }
+ }
+
final boolean selectionChangedOrSafeToReset = oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection
// changed
|| !mWordComposer.isComposingWord(); // safe to reset
@@ -2214,26 +2223,17 @@ private void handleNonSeparatorEvent(final Event event, final SettingsValues set
if (helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.isEnabled(mLatinIME)
&& helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.isImmediateEnabled(mLatinIME)) {
final String typedWord = mWordComposer.getTypedWord();
- final String prefix = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getPrefix(mLatinIME);
- if (prefix.isEmpty()) {
- final String expanded = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getExpandedWord(typedWord, mLatinIME);
- if (expanded != null) {
- commitExpandedText(typedWord, expanded);
- resetComposingState(true);
- }
- } else {
- final CharSequence textBefore = mConnection.getTextBeforeCursor(50, 0);
- if (textBefore != null) {
- final String textStr = textBefore.toString();
- final String targetSuffix = prefix + typedWord;
- if (textStr.toLowerCase(java.util.Locale.US).endsWith(targetSuffix.toLowerCase(java.util.Locale.US))) {
- final String expanded = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getExpandedWord(targetSuffix, mLatinIME);
- if (expanded != null) {
- mConnection.deleteTextBeforeCursor(prefix.length());
- commitExpandedText(targetSuffix, expanded);
- resetComposingState(true);
- }
+ final CharSequence textBefore = mConnection.getTextBeforeCursor(50, 0);
+ if (textBefore != null) {
+ final String textStr = textBefore.toString();
+ final helium314.keyboard.latin.utils.TextExpanderUtils.ExpandedResult result =
+ helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getExpandedWordForTyped(typedWord, textStr, mLatinIME);
+ if (result != null) {
+ if (result.getPrefixLength() > 0) {
+ mConnection.deleteTextBeforeCursor(result.getPrefixLength());
}
+ commitExpandedText(result.getMatchedString(), result.getExpandedText());
+ resetComposingState(true);
}
}
}
@@ -2519,6 +2519,28 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu
}
}
+ if (mLastExpandedText != null && !event.isKeyRepeat()) {
+ final int expectedCursor = mConnection.getExpectedSelectionEnd();
+ if (expectedCursor == mLastExpandedCursorPosition) {
+ final int beforeLen = mLastExpandedCursorOffset;
+ final int afterLen = mLastExpandedText.length() - beforeLen;
+ final CharSequence textBefore = mConnection.getTextBeforeCursor(beforeLen, 0);
+ final CharSequence textAfter = mConnection.getTextAfterCursor(afterLen, 0);
+ final String expectedBefore = mLastExpandedText.substring(0, beforeLen);
+ final String expectedAfter = mLastExpandedText.substring(beforeLen);
+ if (textBefore != null && textBefore.toString().equals(expectedBefore)
+ && textAfter != null && textAfter.toString().equals(expectedAfter)) {
+ mConnection.setSelection(expectedCursor - beforeLen, expectedCursor + afterLen);
+ mConnection.commitText(mLastShortcutText, 1);
+ mLastExpandedText = null;
+ mLastShortcutText = null;
+ mLastExpandedCursorPosition = -1;
+ mLastExpandedCursorOffset = -1;
+ return;
+ }
+ }
+ }
+
// In many cases after backspace, we need to update the shift state. Normally we
// need
// to do this right away to avoid the shift state being out of date in case the
@@ -4265,29 +4287,17 @@ private void commitChosenWord(final SettingsValues settingsValues, final String
// can't find any drawback (performance, neither when setting nor when reading)
final boolean isEnabled = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.isEnabled(mLatinIME);
if (isEnabled) {
- final String prefix = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getPrefix(mLatinIME);
- if (prefix.isEmpty()) {
- final String expanded = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getExpandedWord(chosenWord, mLatinIME);
- if (expanded != null) {
+ final CharSequence textBefore = mConnection.getTextBeforeCursor(50, 0);
+ if (textBefore != null) {
+ final String textStr = textBefore.toString();
+ final helium314.keyboard.latin.utils.TextExpanderUtils.ExpandedResult result =
+ helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getExpandedWordForTyped(chosenWord, textStr, mLatinIME);
+ if (result != null) {
mConnection.commitText(getTextWithSuggestionSpan(mLatinIME, chosenWord, mSuggestedWords, getDictionaryFacilitatorLocale()), 1);
- mConnection.deleteTextBeforeCursor(chosenWord.length());
- commitExpandedText(chosenWord, expanded);
+ mConnection.deleteTextBeforeCursor(result.getPrefixLength() + chosenWord.length());
+ commitExpandedText(result.getMatchedString(), result.getExpandedText());
return;
- }
- } else {
- final CharSequence textBefore = mConnection.getTextBeforeCursor(50, 0);
- if (textBefore != null) {
- final String textStr = textBefore.toString();
- final String targetSuffix = prefix + chosenWord;
- if (textStr.toLowerCase(java.util.Locale.US).endsWith(targetSuffix.toLowerCase(java.util.Locale.US))) {
- final String expanded = helium314.keyboard.latin.utils.TextExpanderUtils.INSTANCE.getExpandedWord(targetSuffix, mLatinIME);
- if (expanded != null) {
- mConnection.commitText(getTextWithSuggestionSpan(mLatinIME, chosenWord, mSuggestedWords, getDictionaryFacilitatorLocale()), 1);
- mConnection.deleteTextBeforeCursor(prefix.length() + chosenWord.length());
- commitExpandedText(targetSuffix, expanded);
- return;
- }
- }
+
}
}
}
@@ -4409,7 +4419,7 @@ public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestio
// we used to provide keyboard, settingsValues and keyboardShiftMode, but every
// time read it from current instance anyway
- void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
+ public void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
if (keyboard == null) {
callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
index c0c14c172..4fa40c105 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
@@ -185,6 +185,8 @@ object Defaults {
const val PREF_LANGUAGE_SWIPE_DISTANCE = 5
const val PREF_TOUCHPAD_SENSITIVITY = 50
const val PREF_TOUCHPAD_EDGE_SCROLL = false
+
+ const val PREF_TOUCHPAD_FULLSCREEN = false
const val PREF_FORCE_AUTO_CAPS = false
const val PREF_OFFLINE_TEMP = 0.1f // Lower for faster, more deterministic proofreading
const val PREF_OFFLINE_TOP_P = 0.5f // Lower for faster token sampling
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
index 64f1c3b70..3755f0b3f 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
@@ -216,6 +216,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_LANGUAGE_SWIPE_DISTANCE = "language_swipe_distance";
public static final String PREF_TOUCHPAD_SENSITIVITY = "touchpad_sensitivity";
public static final String PREF_TOUCHPAD_EDGE_SCROLL = "touchpad_edge_scroll";
+ public static final String PREF_TOUCHPAD_FULLSCREEN = "touchpad_fullscreen";
public static final String PREF_PERSIST_FLOATING_KEYBOARD = "persist_floating_keyboard";
public static final String PREF_FORCE_AUTO_CAPS = "force_auto_caps";
public static final String PREF_OFFLINE_TEMP = "offline_temp";
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
index 5101f5a71..b4e57e442 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
@@ -83,6 +83,7 @@ public class SettingsValues {
public final int mLanguageSwipeDistance;
public final int mTouchpadSensitivity;
public final boolean mTouchpadEdgeScroll;
+ public final boolean mTouchpadFullscreen;
public final boolean mForceAutoCaps;
public final boolean mDeleteSwipeEnabled;
public final boolean mShortcutRowsEnabled;
@@ -442,6 +443,8 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
Defaults.PREF_TOUCHPAD_SENSITIVITY);
mTouchpadEdgeScroll = prefs.getBoolean(Settings.PREF_TOUCHPAD_EDGE_SCROLL,
Defaults.PREF_TOUCHPAD_EDGE_SCROLL);
+ mTouchpadFullscreen = prefs.getBoolean(Settings.PREF_TOUCHPAD_FULLSCREEN,
+ Defaults.PREF_TOUCHPAD_FULLSCREEN);
mForceAutoCaps = prefs.getBoolean(Settings.PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS);
mDeleteSwipeEnabled = prefs.getBoolean(Settings.PREF_DELETE_SWIPE, Defaults.PREF_DELETE_SWIPE);
mShortcutRowsEnabled = prefs.getBoolean(Settings.PREF_SHORTCUT_ROWS, Defaults.PREF_SHORTCUT_ROWS);
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java
index 37cce0874..13c737a0f 100644
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -320,6 +320,9 @@ private int getSuggestionTextColor(final SuggestedWords suggestedWords,
final int color;
if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION && suggestedWords.mWillAutoCorrect) {
color = mColorAutoCorrect;
+ } else if (suggestedWords.isPrediction() && indexInSuggestedWords == 0) {
+ // ponytail: first word prediction should be colored active/bright
+ color = mColorAutoCorrect;
} else if (isTypedWord && suggestedWords.mTypedWordValid) {
color = mColorValidTypedWord;
} else if (isTypedWord) {
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
index de0e41660..4b00aba67 100644
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
@@ -71,7 +71,6 @@ import helium314.keyboard.latin.utils.showMissingDictionaryComposeDialog
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.settings.SettingsWithoutKey
-import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.math.min
@@ -698,30 +697,21 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
return true
}
Settings.getValues().mColors.setColor(icon, ColorType.REMOVE_SUGGESTION_ICON)
- val w = icon.intrinsicWidth
- val h = icon.intrinsicHeight
wordView.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
wordView.ellipsize = TextUtils.TruncateAt.END
- val downOk = AtomicBoolean(false)
- wordView.setOnTouchListener { _, motionEvent ->
- if (motionEvent.action == MotionEvent.ACTION_UP && downOk.get()) {
- val x = motionEvent.x
- val y = motionEvent.y
- if (0 < x && x < w && 0 < y && y < h) {
- removeSuggestion(wordView)
- wordView.cancelLongPress()
- wordView.isPressed = false
- return@setOnTouchListener true
- }
- } else if (motionEvent.action == MotionEvent.ACTION_DOWN) {
- val x = motionEvent.x
- val y = motionEvent.y
- if (0 < x && x < w && 0 < y && y < h) {
- downOk.set(true)
- }
- }
- false
+ // ponytail: entire word view is now the delete target, not just the tiny icon
+ val savedTag = wordView.tag
+ // Replace click listener to delete on any tap
+ wordView.setOnClickListener {
+ removeSuggestion(wordView)
}
+ // Auto-dismiss delete mode after 3s, restore normal behavior
+ wordView.postDelayed({
+ wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
+ wordView.setOnTouchListener(null)
+ wordView.tag = savedTag
+ wordView.setOnClickListener(this)
+ }, 3000)
}
if (DebugFlags.DEBUG_ENABLED && (isShowingMoreSuggestionPanel || !showMoreSuggestions())) {
showSourceDict(wordView)
@@ -925,7 +915,7 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
}
// ponytail: show/hide dictionary download button if dictionary is missing
- if (helium314.keyboard.latin.BuildConfig.FLAVOR == "standard") {
+ if (helium314.keyboard.latin.BuildConfig.FLAVOR == "standard" || helium314.keyboard.latin.BuildConfig.FLAVOR == "standardfull") {
val currentLocale = SubtypeSettings.getSelectedSubtype(context.prefs()).locale()
if (isMainDictionaryMissing(context, currentLocale) && !hideToolbarKeys) {
if (dictDownloadButton == null) {
@@ -964,7 +954,6 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
} else {
dictDownloadButton?.isVisible = false
}
-
isExternalSuggestionVisible = false
}
@@ -1045,64 +1034,11 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
}
suggestionsStrip.isVisible = true
+ // ponytail: no fallback suggestions to keep it clean and minimal
val PLACEHOLDER_TAG = "PLACEHOLDER_VIEW"
val placeholder = suggestionsStrip.findViewWithTag(PLACEHOLDER_TAG)
-
- // Check if there are any visible suggestions with actual text content
- var hasRealSuggestions = false
- for (i in 0 until suggestionsStrip.childCount) {
- val child = suggestionsStrip.getChildAt(i)
- if (child.tag != PLACEHOLDER_TAG && child is TextView && !child.text.isNullOrEmpty()) {
- hasRealSuggestions = true
- break
- }
- }
-
- if (hasRealSuggestions) {
- // Real suggestions exist, remove placeholder
- if (placeholder != null) suggestionsStrip.removeView(placeholder)
- } else {
- // No suggestions, show random placeholder suggestions
- if (placeholder == null) {
- val placeholderContainer = LinearLayout(context)
- placeholderContainer.tag = PLACEHOLDER_TAG
- placeholderContainer.orientation = LinearLayout.HORIZONTAL
- placeholderContainer.layoutParams = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.MATCH_PARENT
- )
-
- // Random suggestion words to display
- val randomSuggestions = listOf(
- "the", "and", "for", "you", "with",
- "have", "this", "from", "will", "can",
- "hello", "thanks", "please", "okay", "good"
- ).shuffled().take(5)
-
- val colors = Settings.getValues().mColors
- val customTypeface = Settings.getInstance().customTypeface
-
- randomSuggestions.forEach { word ->
- val suggestionView = TextView(context, null, R.attr.suggestionWordStyle)
- suggestionView.text = word
- suggestionView.gravity = android.view.Gravity.CENTER
- suggestionView.alpha = 0.4f // More transparent to indicate they're placeholders
- if (customTypeface != null)
- suggestionView.typeface = customTypeface
- colors.setBackground(suggestionView, ColorType.STRIP_BACKGROUND)
- suggestionView.setTextColor(colors.get(ColorType.KEY_TEXT))
-
- val params = LinearLayout.LayoutParams(
- 0,
- LinearLayout.LayoutParams.MATCH_PARENT,
- 1f
- )
- suggestionView.layoutParams = params
- placeholderContainer.addView(suggestionView)
- }
-
- suggestionsStrip.addView(placeholderContainer)
- }
+ if (placeholder != null) {
+ suggestionsStrip.removeView(placeholder)
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
index b23d5e90a..03ab34a48 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
@@ -112,8 +112,22 @@ object DictionaryInfoUtils {
fun getCachedDictForLocaleAndType(locale: Locale, type: String, context: Context): File? =
getCachedDictsForLocale(locale, context).firstOrNull { it.name.substringBefore("_") == type }
- fun getCachedDictsForLocale(locale: Locale, context: Context) =
- getCacheDirectoryForLocale(locale, context)?.let { File(it).listFiles() }.orEmpty()
+ fun getCachedDictsForLocale(locale: Locale, context: Context): Array {
+ val exactDir = getCacheDirectoryForLocale(locale, context)?.let { File(it) }
+ val exactFiles = exactDir?.listFiles()
+ if (exactFiles?.any { it.name.endsWith(USER_DICTIONARY_SUFFIX) || it.name.startsWith(MAIN_DICT_PREFIX) || it.name == MAIN_DICT_FILE_NAME } == true) {
+ return exactFiles
+ }
+ if (locale.country.isNotEmpty() || locale.variant.isNotEmpty()) {
+ val fallbackLocale = Locale(locale.language)
+ val fallbackDir = getCacheDirectoryForLocale(fallbackLocale, context)?.let { File(it) }
+ val fallbackFiles = fallbackDir?.listFiles()
+ if (fallbackFiles?.any { it.name.endsWith(USER_DICTIONARY_SUFFIX) || it.name.startsWith(MAIN_DICT_PREFIX) || it.name == MAIN_DICT_FILE_NAME } == true) {
+ return fallbackFiles
+ }
+ }
+ return exactFiles ?: emptyArray()
+ }
fun getDictionaryFileHeaderOrNull(file: File): DictionaryHeader? {
return try {
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
index 667a12c84..c66942fe7 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
@@ -53,25 +53,54 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner
fun getDictionaryLocales(context: Context): MutableSet {
val locales = HashSet()
+ // ponytail: migrate legacy incorrectly-named dictionary folders (gb, au, ca) to en-GB, en-AU, en-CA
+ val dictDir = File(DictionaryInfoUtils.getWordListCacheDirectory(context))
+ if (dictDir.exists() && dictDir.isDirectory) {
+ val legacyMap = mapOf("gb" to "en-GB", "au" to "en-AU", "ca" to "en-CA")
+ legacyMap.forEach { (legacy, correct) ->
+ val legacyFolder = File(dictDir, legacy)
+ if (legacyFolder.exists() && legacyFolder.isDirectory) {
+ val correctFolder = File(dictDir, correct)
+ if (!correctFolder.exists()) {
+ legacyFolder.renameTo(correctFolder)
+ } else {
+ legacyFolder.deleteRecursively()
+ }
+ }
+ }
+ }
+
+ // ponytail: include enabled locales and multilingual secondary locales
+ val enabled = SubtypeSettings.getEnabledSubtypes(true)
+ val enabledLocales = HashSet()
+ enabled.forEach { subtype ->
+ enabledLocales.add(subtype.locale())
+ getSecondaryLocales(subtype.extraValue).forEach { enabledLocales.add(it) }
+ }
+ android.util.Log.i("DictionaryUtils", "getDictionaryLocales: enabledLocales=$enabledLocales")
+
// ponytail: get cached dictionaries: extracted or user-added/downloaded dictionaries
DictionaryInfoUtils.getCacheDirectories(context).forEach { directory ->
if (!hasAnythingOtherThanExtractedMainDictionary(context, directory)) return@forEach
val locale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name).constructLocale()
+ val isEnabled = enabledLocales.contains(locale)
+ val hasEnabledLanguage = enabledLocales.any { it.language == locale.language }
+ android.util.Log.i("DictionaryUtils", "Cache loop: locale=$locale, isEnabled=$isEnabled, hasEnabledLanguage=$hasEnabledLanguage")
+ if (!isEnabled && hasEnabledLanguage) return@forEach
locales.add(locale)
}
// get assets dictionaries
val assetsDictionaryList = DictionaryInfoUtils.getAssetsDictionaryList(context)
if (assetsDictionaryList != null) {
for (dictionary in assetsDictionaryList) {
- locales.add(DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary))
+ val locale = DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary)
+ val isEnabled = enabledLocales.contains(locale)
+ val hasEnabledLanguage = enabledLocales.any { it.language == locale.language }
+ if (!isEnabled && hasEnabledLanguage) continue
+ locales.add(locale)
}
}
- // ponytail: include enabled locales and multilingual secondary locales
- val enabled = SubtypeSettings.getEnabledSubtypes()
- enabled.forEach { subtype ->
- locales.add(subtype.locale())
- getSecondaryLocales(subtype.extraValue).forEach { locales.add(it) }
- }
+ locales.addAll(enabledLocales)
return locales
}
@@ -91,7 +120,7 @@ fun MissingDictionaryDialog(onDismissRequest: () -> Unit, locale: Locale, inline
var annotatedString = message.htmlToAnnotated()
// ponytail: in standard flavor, if there are known dicts we show them as downloadable rows instead of bullet links
val knownDicts = remember {
- if (helium314.keyboard.latin.BuildConfig.FLAVOR == "standard") {
+ if (helium314.keyboard.latin.BuildConfig.FLAVOR == "standard" || helium314.keyboard.latin.BuildConfig.FLAVOR == "standardfull") {
getKnownDictionariesForLocale(locale, context)
} else emptyList()
}
@@ -237,7 +266,12 @@ fun downloadDictionary(context: Context, locale: Locale, type: String, linkUrl:
fun DownloadableDictionaryRow(locale: Locale, desc: String, link: String, onRefresh: () -> Unit) {
val ctx = LocalContext.current
val type = remember(link) { link.substringAfterLast("/").substringBefore("_") }
- val cacheDir = remember(locale) { DictionaryInfoUtils.getCacheDirectoryForLocale(locale, ctx) }
+ // ponytail: extract the specific dictionary locale from the download link to avoid directory collision
+ val dictLocale = remember(link) {
+ val fileName = link.substringAfterLast("/")
+ fileName.substringAfter("_").substringBefore(".dict").constructLocale()
+ }
+ val cacheDir = remember(dictLocale) { DictionaryInfoUtils.getCacheDirectoryForLocale(dictLocale, ctx) }
val file = remember(cacheDir, type) { cacheDir?.let { File(it, "$type.dict") } }
var downloading by remember { mutableStateOf(false) }
var exists by remember(file) { mutableStateOf(file?.exists() == true) }
@@ -274,7 +308,7 @@ fun DownloadableDictionaryRow(locale: Locale, desc: String, link: String, onRefr
} else {
androidx.compose.material3.TextButton(onClick = {
downloading = true
- downloadDictionary(ctx, locale, type, link) { success ->
+ downloadDictionary(ctx, dictLocale, type, link) { success ->
downloading = false
if (success) {
exists = true
@@ -301,11 +335,14 @@ fun isMainDictionaryMissing(context: Context, locale: Locale): Boolean {
if (best != null) return false
}
// 2. check if cache directory has a main.dict file
- val cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, context)?.let { File(it) }
- if (cacheDir?.exists() == true && cacheDir.isDirectory) {
- val hasMain = cacheDir.listFiles()?.any { it.name == "main.dict" } == true
- if (hasMain) return false
+ var cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, context)?.let { File(it) }
+ var hasMain = cacheDir?.exists() == true && cacheDir.isDirectory && cacheDir.listFiles()?.any { it.name == "main.dict" } == true
+ if (!hasMain && (locale.country.isNotEmpty() || locale.variant.isNotEmpty())) {
+ val fallbackLocale = Locale(locale.language)
+ cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(fallbackLocale, context)?.let { File(it) }
+ hasMain = cacheDir?.exists() == true && cacheDir.isDirectory && cacheDir.listFiles()?.any { it.name == "main.dict" } == true
}
+ if (hasMain) return false
// 3. check if there is a known downloadable main dictionary for this locale
val known = getKnownDictionariesForLocale(locale, context)
return known.any { (_, link) -> link.substringAfterLast("/").substringBefore("_") == "main" }
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt
index eff85bce3..9df8b7bdd 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt
@@ -28,33 +28,54 @@ object TextExpanderUtils {
}
fun getPrefix(context: Context): String {
- return context.prefs().getString(PREF_PREFIX, "") ?: ""
+ return ""
}
- fun getShortcuts(context: Context): Map {
+ data class ShortcutEntry(
+ val template: String,
+ val prefix: String = ""
+ )
+
+ data class ExpandedResult(
+ val expandedText: String,
+ val prefixLength: Int,
+ val matchedString: String
+ )
+
+ fun getShortcuts(context: Context): Map {
val jsonStr = context.prefs().getString(PREF_DATA, "{}") ?: "{}"
- val map = mutableMapOf()
+ val map = mutableMapOf()
try {
val json = JSONObject(jsonStr)
val keys = json.keys()
while (keys.hasNext()) {
val key = keys.next()
- map[key] = json.getString(key)
+ val valueObj = json.get(key)
+ if (valueObj is JSONObject) {
+ val template = valueObj.optString("template", "")
+ val prefix = valueObj.optString("prefix", "")
+ map[key] = ShortcutEntry(template, prefix)
+ } else {
+ map[key] = ShortcutEntry(valueObj.toString(), "")
+ }
}
- } catch (e: Exception) {
+ } catch (e: java.lang.Exception) {
// fallback
}
return map
}
- fun saveShortcuts(context: Context, map: Map) {
+ fun saveShortcuts(context: Context, map: Map) {
try {
val json = JSONObject()
- for ((key, value) in map) {
- json.put(key, value)
+ for ((key, entry) in map) {
+ val obj = JSONObject()
+ obj.put("template", entry.template)
+ obj.put("prefix", entry.prefix)
+ json.put(key, obj)
}
context.prefs().edit().putString(PREF_DATA, json.toString()).apply()
- } catch (e: Exception) {
+ } catch (e: java.lang.Exception) {
// fail silently
}
}
@@ -190,38 +211,49 @@ object TextExpanderUtils {
return result
}
- fun getExpandedWord(word: String?, context: Context): String? {
- if (word == null || !isEnabled(context)) return null
-
- val prefix = getPrefix(context)
- if (prefix.isNotEmpty() && !word.startsWith(prefix)) return null
-
- val shortcut = if (prefix.isNotEmpty()) word.substring(prefix.length) else word
- if (shortcut.isEmpty()) return null
-
+ fun getExpandedWordForTyped(word: String?, textBeforeCursor: String?, context: Context): ExpandedResult? {
+ if (word == null || textBeforeCursor == null || !isEnabled(context)) return null
val shortcuts = getShortcuts(context)
- // Check exact match or lowercase match
- val template = shortcuts[shortcut] ?: shortcuts[shortcut.lowercase(Locale.getDefault())]
- if (template != null) {
- return expand(template, context)
- }
- // Check regex matches
- for ((key, value) in shortcuts) {
- if (key.startsWith(REGEX_PREFIX)) {
- val patternStr = key.substring(REGEX_PREFIX.length)
+ for ((key, entry) in shortcuts) {
+ val isRegex = key.startsWith(REGEX_PREFIX)
+ val cleanKey = if (isRegex) key.substring(REGEX_PREFIX.length) else key
+
+ if (isRegex) {
+ val prefix = entry.prefix
+ val patternStr = cleanKey
try {
val regex = Regex(patternStr, RegexOption.IGNORE_CASE)
- if (regex.matches(shortcut)) {
- val replaced = regex.replace(shortcut, value)
- return expand(replaced, context)
+ val expectedSuffix = prefix + word
+ if (textBeforeCursor.endsWith(expectedSuffix, ignoreCase = true)) {
+ if (regex.matches(expectedSuffix)) {
+ val replaced = regex.replace(expectedSuffix, entry.template)
+ return ExpandedResult(expand(replaced, context), prefix.length, expectedSuffix)
+ }
+ }
+ } catch (e: java.lang.Exception) {
+ // ignore
+ }
+ } else {
+ val prefix = entry.prefix
+ val expectedSuffix = cleanKey
+ if (expectedSuffix.equals(prefix + word, ignoreCase = true)) {
+ if (textBeforeCursor.endsWith(expectedSuffix, ignoreCase = true)) {
+ return ExpandedResult(expand(entry.template, context), prefix.length, expectedSuffix)
}
- } catch (e: Exception) {
- // ignore invalid regex
}
}
}
-
+ return null
+ }
+
+ fun getExpandedWord(word: String?, context: Context): String? {
+ if (word == null || !isEnabled(context)) return null
+ val shortcuts = getShortcuts(context)
+ val entry = shortcuts[word] ?: shortcuts[word.lowercase(Locale.getDefault())]
+ if (entry != null && entry.prefix.isEmpty()) {
+ return expand(entry.template, context)
+ }
return null
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
index 54ed101bf..23c3b49b7 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
@@ -281,6 +281,7 @@ fun getCodeForToolbarKey(key: ToolbarKey) = Settings.getInstance().getCustomTool
FLOATING -> KeyCode.TOGGLE_FLOATING_KEYBOARD
INCOGNITO -> KeyCode.TOGGLE_INCOGNITO_MODE
TOUCHPAD -> KeyCode.TOGGLE_TOUCHPAD_MODE
+ TEXT_EDIT -> KeyCode.TOGGLE_TEXT_EDIT_MODE
AUTOCORRECT -> KeyCode.TOGGLE_AUTOCORRECT
AUTOSPACE -> KeyCode.TOGGLE_AUTOSPACE
AUTO_CAP -> KeyCode.TOGGLE_AUTO_CAP
@@ -341,7 +342,7 @@ fun getCodeForToolbarKeyLongClick(key: ToolbarKey) = Settings.getInstance().getC
// names need to be aligned with resources strings (using lowercase of key.name)
enum class ToolbarKey {
VOICE, CLIPBOARD, CLIPBOARD_SEARCH, NUMPAD, HANDWRITING, UNDO, REDO, SETTINGS, SELECT_ALL, SELECT_WORD, COPY, CUT, PASTE, ONE_HANDED, SPLIT, FLOATING,
- INCOGNITO, TOUCHPAD, AUTOCORRECT, AUTOSPACE, AUTO_CAP, FORCE_AUTO_CAP, CLEAR_CLIPBOARD, CLOSE_HISTORY, EMOJI, LEFT, RIGHT, UP, DOWN, WORD_LEFT, WORD_RIGHT,
+ INCOGNITO, TOUCHPAD, TEXT_EDIT, AUTOCORRECT, AUTOSPACE, AUTO_CAP, FORCE_AUTO_CAP, CLEAR_CLIPBOARD, CLOSE_HISTORY, EMOJI, LEFT, RIGHT, UP, DOWN, WORD_LEFT, WORD_RIGHT,
PAGE_UP, PAGE_DOWN, FULL_LEFT, FULL_RIGHT, PAGE_START, PAGE_END, JOIN_NEXT, FORCE_NEXT_SPACE, UNDO_WORD, PROOFREAD, TRANSLATE,
CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, CUSTOM_AI_4, CUSTOM_AI_5,
CUSTOM_AI_6, CUSTOM_AI_7, CUSTOM_AI_8, CUSTOM_AI_9, CUSTOM_AI_10
@@ -355,12 +356,12 @@ val toolbarKeyStrings = entries.associateWithTo(EnumMap(ToolbarKey::class.java))
// ponytail: Split excluded keys into flavor-specific exclusions and main-toolbar-only exclusions to allow clipboard toolbar to render clipboard search and close history.
private val flavorExcludedKeys by lazy {
- val customAiKeys = if (BuildConfig.FLAVOR != "standard" && BuildConfig.FLAVOR != "offline")
+ val customAiKeys = if (BuildConfig.FLAVOR != "standard" && BuildConfig.FLAVOR != "standardfull" && BuildConfig.FLAVOR != "offline")
ToolbarKey.entries.filter { it.name.startsWith("CUSTOM_AI_") }
else emptyList()
val otherKeys = if (BuildConfig.FLAVOR == "offlinelite")
listOf(PROOFREAD, TRANSLATE, CLIPBOARD_SEARCH, HANDWRITING)
- else if (BuildConfig.FLAVOR == "offline")
+ else if (BuildConfig.FLAVOR == "offline" || BuildConfig.FLAVOR == "standard")
listOf(HANDWRITING)
else
emptyList()
@@ -375,9 +376,9 @@ private val excludedKeys by lazy {
val defaultToolbarPref by lazy {
val default = when (helium314.keyboard.latin.BuildConfig.FLAVOR) {
- "offline" -> listOf(SETTINGS, VOICE, CLIPBOARD, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, UNDO, INCOGNITO, COPY, PASTE, PROOFREAD, TRANSLATE)
+ "offline" -> listOf(SETTINGS, VOICE, CLIPBOARD, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, UNDO, INCOGNITO, COPY, PASTE, PROOFREAD, TRANSLATE, TEXT_EDIT)
"offlinelite" -> listOf(SETTINGS, VOICE, CLIPBOARD, UNDO, INCOGNITO, COPY, PASTE)
- else -> listOf(SETTINGS, VOICE, CLIPBOARD, HANDWRITING, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, UNDO, PROOFREAD, TRANSLATE, INCOGNITO, TOUCHPAD, FLOATING, NUMPAD, COPY, PASTE, SELECT_ALL)
+ else -> listOf(SETTINGS, VOICE, CLIPBOARD, HANDWRITING, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, UNDO, PROOFREAD, TRANSLATE, INCOGNITO, TOUCHPAD, TEXT_EDIT, FLOATING, NUMPAD, COPY, PASTE, SELECT_ALL)
}
val others = entries.filterNot { it in default || it in excludedKeys }
@@ -388,7 +389,7 @@ val defaultToolbarPref by lazy {
val defaultPinnedToolbarPref by lazy {
val pinnedDefault = when (helium314.keyboard.latin.BuildConfig.FLAVOR) {
"offlinelite" -> listOf(CLIPBOARD)
- else -> listOf(CLIPBOARD, PROOFREAD, TOUCHPAD, FLOATING)
+ else -> listOf(CLIPBOARD, PROOFREAD, TOUCHPAD, TEXT_EDIT, FLOATING)
}
entries.filterNot { it in excludedKeys }.joinToString(Separators.ENTRY) {
diff --git a/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt b/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt
index a38dc070b..542de0860 100644
--- a/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt
+++ b/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt
@@ -266,7 +266,7 @@ fun WelcomeWizard(
{ step++ },
{ step-- }
) {
- if (BuildConfig.FLAVOR == "standard") {
+ if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardfull") {
val service = remember { helium314.keyboard.latin.utils.ProofreadService(ctx) }
var currentProvider by remember { mutableStateOf(service.getProvider()) }
val aiConfigured = when (currentProvider) {
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
index c4219624a..2c5a98c5d 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
@@ -60,7 +60,10 @@ fun DictionaryDialog(
val ctx = LocalContext.current
var refreshTrigger by remember { mutableStateOf(0) }
val (dictionaries, hasInternal) = remember(refreshTrigger) { getUserAndInternalDictionaries(ctx, locale) }
- val mainDict = dictionaries.firstOrNull { it.name == Dictionary.TYPE_MAIN + "_" + DictionaryInfoUtils.USER_DICTIONARY_SUFFIX }
+ val mainDict = dictionaries.firstOrNull {
+ it.name == Dictionary.TYPE_MAIN + "_" + DictionaryInfoUtils.USER_DICTIONARY_SUFFIX
+ || it.name == DictionaryInfoUtils.MAIN_DICT_FILE_NAME
+ }
val addonDicts = dictionaries.filterNot { it == mainDict }
val picker = dictionaryFilePicker(locale)
ThreeButtonAlertDialog(
@@ -71,55 +74,41 @@ fun DictionaryDialog(
title = { Text(locale.localizedDisplayName(LocalResources.current)) },
content = {
Column {
- if (hasInternal) {
- val internalDicts = DictionaryInfoUtils.getAssetsDictionaryList(ctx)
- val best = internalDicts?.let {
- LocaleUtils.getBestMatch(locale, it.toList()) { dict ->
- DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dict)
- }
+ val internalDicts = DictionaryInfoUtils.getAssetsDictionaryList(ctx)
+ val best = internalDicts?.let {
+ LocaleUtils.getBestMatch(locale, it.toList()) { dict ->
+ DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dict)
}
- val internalId = best?.let { "main:" + it.substringAfter("_").substringBefore(".") }
+ }
+ val internalId = best?.let { "main:" + it.substringAfter("_").substringBefore(".") }
+ val mainPrefKey = "pref_dict_enabled_" + (internalId ?: "main:${locale.language}")
- val color = if (mainDict == null) MaterialTheme.typography.titleSmall.color
- else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // for disabled look
- val bottomPadding = if (mainDict == null) 12.dp else 0.dp
+ val prefs = ctx.prefs()
+ var enabled by remember { mutableStateOf(prefs.getBoolean(mainPrefKey, true)) }
- if (internalId != null) {
- val prefs = ctx.prefs()
- val prefKey = "pref_dict_enabled_$internalId"
- var enabled by remember { mutableStateOf(prefs.getBoolean(prefKey, true)) }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = bottomPadding)
- ) {
- Switch(
- checked = enabled && (mainDict == null),
- enabled = mainDict == null,
- onCheckedChange = { isChecked ->
- enabled = isChecked
- prefs.edit().putBoolean(prefKey, isChecked).apply()
- },
- modifier = Modifier.padding(end = 8.dp)
- )
- Text(stringResource(R.string.internal_dictionary_summary),
- color = color,
- style = MaterialTheme.typography.titleSmall
- )
- }
- } else {
- Text(stringResource(R.string.internal_dictionary_summary),
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = bottomPadding),
- color = color,
- style = MaterialTheme.typography.titleSmall
- )
- }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp)
+ ) {
+ Switch(
+ checked = enabled,
+ onCheckedChange = { isChecked ->
+ enabled = isChecked
+ prefs.edit().putBoolean(mainPrefKey, isChecked).apply()
+ },
+ modifier = Modifier.padding(end = 8.dp)
+ )
+ Text(
+ text = stringResource(R.string.main_dictionary),
+ style = MaterialTheme.typography.titleMedium
+ )
}
- if (mainDict != null)
+
+ if (mainDict != null) {
DictionaryDetails(mainDict) { refreshTrigger++ }
+ }
if (addonDicts.isNotEmpty()) {
HorizontalDivider()
Text(stringResource(R.string.dictionary_category_title),
@@ -129,7 +118,7 @@ fun DictionaryDialog(
addonDicts.forEach { DictionaryDetails(it) { refreshTrigger++ } }
}
val knownDicts = remember {
- if (helium314.keyboard.latin.BuildConfig.FLAVOR == "standard") {
+ if (helium314.keyboard.latin.BuildConfig.FLAVOR == "standard" || helium314.keyboard.latin.BuildConfig.FLAVOR == "standardfull") {
helium314.keyboard.latin.utils.getKnownDictionariesForLocale(locale, ctx)
} else emptyList()
}
@@ -172,15 +161,16 @@ fun DictionaryDialog(
private fun DictionaryDetails(dict: File, onDelete: () -> Unit) {
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dict) ?: return
val type = header.mIdString.substringBefore(":")
- var showDeleteDialog by remember { mutableStateOf(false) }
- var showDetails by remember { mutableStateOf(false) }
- val title = if (type != DictionaryInfoUtils.DEFAULT_MAIN_DICT) type
- else stringResource(R.string.main_dictionary)
val ctx = LocalContext.current
val prefs = ctx.prefs()
val prefKey = "pref_dict_enabled_${header.mIdString}"
var enabled by remember { mutableStateOf(prefs.getBoolean(prefKey, true)) }
-
+ var showDetails by remember { mutableStateOf(false) }
+ val title = when (type) {
+ DictionaryInfoUtils.DEFAULT_MAIN_DICT -> stringResource(R.string.main_dictionary)
+ Dictionary.TYPE_EMOJI -> stringResource(R.string.subtype_emoji)
+ else -> type
+ }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -195,10 +185,8 @@ private fun DictionaryDetails(dict: File, onDelete: () -> Unit) {
modifier = Modifier.padding(end = 8.dp)
)
Text(title, style = MaterialTheme.typography.titleSmall, modifier = Modifier.weight(1f))
- DeleteButton { showDeleteDialog = true }
ExpandButton { showDetails = !showDetails }
}
- // default animations look better but make the dialog flash, see also MultiSliderPreference
AnimatedVisibility(showDetails, enter = fadeIn(), exit = fadeOut()) {
Text(
header.info(LocalConfiguration.current.locale()),
@@ -206,16 +194,7 @@ private fun DictionaryDetails(dict: File, onDelete: () -> Unit) {
modifier = Modifier.padding(start = 10.dp, top = 0.dp, end = 10.dp, bottom = 12.dp)
)
}
- if (showDeleteDialog)
- ConfirmationDialog(
- onDismissRequest = { showDeleteDialog = false },
- confirmButtonText = stringResource(R.string.remove),
- onConfirmed = {
- dict.delete()
- onDelete()
- },
- content = { Text(stringResource(R.string.remove_dictionary_message, type))}
- )
+
}
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AIIntegrationScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AIIntegrationScreen.kt
index e1df6b1e0..b78b0e90b 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AIIntegrationScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AIIntegrationScreen.kt
@@ -27,7 +27,7 @@ fun AIIntegrationScreen(
return
}
- if (BuildConfig.FLAVOR == "standard") {
+ if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardfull") {
StandardAIIntegrationScreen(onClickBack)
} else {
OfflineAIIntegrationScreen(onClickBack)
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt
index c5db7023b..372ea599f 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt
@@ -536,7 +536,7 @@ fun createAdvancedSettings(context: Context) = listOfNotNull(
)
}
},
- if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "offline") Setting(context, SettingsWithoutKey.CUSTOM_AI_KEYS, R.string.custom_ai_keys_title, R.string.custom_ai_keys_summary) {
+ if (BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardfull" || BuildConfig.FLAVOR == "offline") Setting(context, SettingsWithoutKey.CUSTOM_AI_KEYS, R.string.custom_ai_keys_title, R.string.custom_ai_keys_summary) {
Preference(
name = it.title,
description = it.description,
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt
index 1f1a27a77..a51cc450d 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt
@@ -3,12 +3,22 @@ package helium314.keyboard.settings.screens
import android.content.Context
import android.content.Intent
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -20,6 +30,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
@@ -84,94 +95,102 @@ fun DictionaryScreen(
},
itemContent = { locale ->
if (locale.language == SubtypeLocaleUtils.NO_LANGUAGE) {
- // Add Dictionary Entry
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ // Card for general actions
+ Card(
modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 16.dp)
.fillMaxWidth()
- .clickable { showAddDictDialog = true }
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ shape = RoundedCornerShape(16.dp)
) {
- Text(
- stringResource(R.string.add_new_dictionary_title),
- )
- Icon(painterResource(R.drawable.ic_plus), stringResource(R.string.add_new_dictionary_title))
- }
- androidx.compose.material3.Divider(modifier = Modifier.padding(vertical = 4.dp))
-
- // Personal Dictionary Entry
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 16.dp)
- .fillMaxWidth()
- .clickable { SettingsDestination.navigateTo(SettingsDestination.PersonalDictionaries) }
- ) {
- Text(
- stringResource(R.string.edit_personal_dictionary),
- style = MaterialTheme.typography.titleMedium
- )
- NextScreenIcon()
- }
- androidx.compose.material3.Divider(modifier = Modifier.padding(vertical = 4.dp))
+ Column(modifier = Modifier.padding(vertical = 4.dp)) {
+ // Add Dictionary Entry
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { showAddDictDialog = true }
+ .padding(vertical = 14.dp, horizontal = 16.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
+ Icon(
+ painter = painterResource(R.drawable.ic_plus),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(end = 12.dp).size(24.dp)
+ )
+ Text(
+ stringResource(R.string.add_new_dictionary_title),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ NextScreenIcon()
+ }
- // Blocked Words Entry
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 16.dp)
- .fillMaxWidth()
- .clickable { SettingsDestination.navigateTo(SettingsDestination.BlockedWords) }
- ) {
- Text(
- stringResource(R.string.edit_blocked_words),
- style = MaterialTheme.typography.titleMedium
- )
- NextScreenIcon()
- }
- androidx.compose.material3.Divider(modifier = Modifier.padding(vertical = 4.dp))
+ HorizontalDivider(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ color = MaterialTheme.colorScheme.outlineVariant
+ )
- // Blocked Words Entry
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 16.dp)
- .fillMaxWidth()
- .clickable { SettingsDestination.navigateTo(SettingsDestination.BlockedWords) }
- ) {
- Text(
- stringResource(R.string.edit_blocked_words),
- style = MaterialTheme.typography.titleMedium
- )
- NextScreenIcon()
- }
- androidx.compose.material3.Divider(modifier = Modifier.padding(vertical = 4.dp))
+ // Personal Dictionary Entry
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { SettingsDestination.navigateTo(SettingsDestination.PersonalDictionaries) }
+ .padding(vertical = 14.dp, horizontal = 16.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
+ Icon(
+ painter = painterResource(R.drawable.ic_dictionary),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(end = 12.dp).size(24.dp)
+ )
+ Text(
+ stringResource(R.string.edit_personal_dictionary),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ NextScreenIcon()
+ }
- // Blocklist Entry
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 16.dp)
- .fillMaxWidth()
- .clickable { SettingsDestination.navigateTo(SettingsDestination.Blocklist) }
- ) {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- stringResource(R.string.blocklist),
- style = MaterialTheme.typography.titleMedium
- )
- Text(
- stringResource(R.string.blocklist_summary),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ HorizontalDivider(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ color = MaterialTheme.colorScheme.outlineVariant
)
+
+ // Blocked Words Entry
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { SettingsDestination.navigateTo(SettingsDestination.BlockedWords) }
+ .padding(vertical = 14.dp, horizontal = 16.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
+ Icon(
+ painter = painterResource(R.drawable.ic_bin),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(end = 12.dp).size(24.dp)
+ )
+ Text(
+ stringResource(R.string.edit_blocked_words),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ NextScreenIcon()
+ }
}
- NextScreenIcon()
}
androidx.compose.material3.Divider(modifier = Modifier.padding(vertical = 4.dp))
@@ -244,58 +263,145 @@ fun DictionaryScreen(
)
}
androidx.compose.material3.Divider(modifier = Modifier.padding(vertical = 4.dp))
-
- // Personal Dictionary Setting
+
+ // Card for Personal Dictionary Switch Setting
val prefs = ctx.prefs()
var personalDictEnabled by remember { mutableStateOf(prefs.getBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, Defaults.PREF_ADD_TO_PERSONAL_DICTIONARY)) }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ Card(
modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 16.dp)
.fillMaxWidth()
- .clickable {
- val newValue = !personalDictEnabled
- personalDictEnabled = newValue
- ctx.prefs().edit { putBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, newValue) }
- }
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ shape = RoundedCornerShape(16.dp)
) {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- stringResource(R.string.add_to_personal_dictionary),
- style = MaterialTheme.typography.titleMedium
- )
- Text(
- stringResource(R.string.add_to_personal_dictionary_summary),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .clickable {
+ val newValue = !personalDictEnabled
+ personalDictEnabled = newValue
+ ctx.prefs().edit { putBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, newValue) }
+ }
+ .padding(all = 16.dp)
+ .fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.weight(1f).padding(end = 16.dp)) {
+ Text(
+ stringResource(R.string.add_to_personal_dictionary),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ stringResource(R.string.add_to_personal_dictionary_summary),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ androidx.compose.material3.Switch(
+ checked = personalDictEnabled,
+ onCheckedChange = {
+ personalDictEnabled = it
+ ctx.prefs().edit { putBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, it) }
+ }
)
}
- androidx.compose.material3.Switch(
- checked = personalDictEnabled,
- onCheckedChange = {
- personalDictEnabled = it
- ctx.prefs().edit { putBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, it) }
- }
- )
}
+
+ // Add a "Languages" Section Header
+ Text(
+ text = stringResource(R.string.language_and_layouts_title),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
+ modifier = Modifier.padding(start = 24.dp, top = 20.dp, bottom = 8.dp)
+ )
} else {
- Column(
- Modifier
- .clickable { selectedLocale = locale }
- .padding(vertical = 6.dp, horizontal = 16.dp)
+ // Premium Language Card
+ Card(
+ modifier = Modifier
.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 6.dp)
+ .clickable { selectedLocale = locale },
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
+ ),
+ shape = RoundedCornerShape(12.dp)
) {
- val (dicts, hasInternal) = getUserAndInternalDictionaries(ctx, locale)
- val types = dicts.mapTo(mutableListOf()) { it.name.substringBefore("_${DictionaryInfoUtils.USER_DICTIONARY_SUFFIX}") }
- if (hasInternal && !types.contains(Dictionary.TYPE_MAIN))
- types.add(0, stringResource(R.string.internal_dictionary_summary))
- Text(locale.localizedDisplayName(LocalResources.current))
- Text(
- types.joinToString(", "),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(all = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = locale.localizedDisplayName(LocalResources.current),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+
+ val (dicts, hasInternal) = getUserAndInternalDictionaries(ctx, locale)
+ val mainDictLabel = stringResource(R.string.main_dictionary)
+ val internalDictLabel = stringResource(R.string.internal_dictionary_summary)
+ val types = dicts.mapTo(mutableListOf()) { file ->
+ if (file.name == DictionaryInfoUtils.MAIN_DICT_FILE_NAME) {
+ mainDictLabel
+ } else {
+ file.name.substringBefore("_${DictionaryInfoUtils.USER_DICTIONARY_SUFFIX}")
+ }
+ }
+ if (hasInternal && !types.contains(Dictionary.TYPE_MAIN) && !types.contains(mainDictLabel))
+ types.add(0, internalDictLabel)
+
+ // Render active dictionaries as stylized badges
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ if (types.isEmpty()) {
+ Text(
+ text = "No active dictionaries",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
+ )
+ } else {
+ types.forEach { type ->
+ val badgeColor = when (type.lowercase()) {
+ "main", internalDictLabel.lowercase(), mainDictLabel.lowercase() -> MaterialTheme.colorScheme.primaryContainer
+ "user" -> MaterialTheme.colorScheme.secondaryContainer
+ else -> MaterialTheme.colorScheme.tertiaryContainer
+ }
+ val badgeTextColor = when (type.lowercase()) {
+ "main", internalDictLabel.lowercase(), mainDictLabel.lowercase() -> MaterialTheme.colorScheme.onPrimaryContainer
+ "user" -> MaterialTheme.colorScheme.onSecondaryContainer
+ else -> MaterialTheme.colorScheme.onTertiaryContainer
+ }
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(6.dp))
+ .background(badgeColor)
+ .padding(horizontal = 8.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = type,
+ style = MaterialTheme.typography.labelMedium,
+ color = badgeTextColor,
+ fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
+ )
+ }
+ }
+ }
+ }
+ }
+ NextScreenIcon()
+ }
}
}
}
@@ -325,26 +431,46 @@ fun DictionaryScreen(
}
}
-/** @return list of user dictionary files and whether an internal dictionary exists */
fun getUserAndInternalDictionaries(context: Context, locale: Locale): Pair, Boolean> {
val userDicts = mutableListOf()
var hasInternalDict = false
- val userLocaleDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, context)?.let { File(it) }
+
+ var userLocaleDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, context)?.let { File(it) }
+ var hasFiles = userLocaleDir?.exists() == true && userLocaleDir.isDirectory && userLocaleDir.listFiles()?.any {
+ it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) || it.name.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX) || it.name.endsWith(".dict")
+ } == true
+
+ if (!hasFiles && (locale.country.isNotEmpty() || locale.variant.isNotEmpty())) {
+ val fallbackLocale = Locale(locale.language)
+ val fallbackDir = DictionaryInfoUtils.getCacheDirectoryForLocale(fallbackLocale, context)?.let { File(it) }
+ val hasFallbackFiles = fallbackDir?.exists() == true && fallbackDir.isDirectory && fallbackDir.listFiles()?.any {
+ it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) || it.name.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX) || it.name.endsWith(".dict")
+ } == true
+ if (hasFallbackFiles) {
+ userLocaleDir = fallbackDir
+ }
+ }
+
if (userLocaleDir?.exists() == true && userLocaleDir.isDirectory) {
userLocaleDir.listFiles()?.forEach {
- if (it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX))
+ if (it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX)) {
userDicts.add(it)
- else if (it.name.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX))
+ } else if (it.name.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX)) {
hasInternalDict = true
+ } else if (it.name.endsWith(".dict")) {
+ userDicts.add(it)
+ }
}
}
- if (hasInternalDict)
- return userDicts to true
- val internalDicts = DictionaryInfoUtils.getAssetsDictionaryList(context) ?: return userDicts to false
- val best = LocaleUtils.getBestMatch(locale, internalDicts.toList()) {
- DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(it)
+ val internalDicts = DictionaryInfoUtils.getAssetsDictionaryList(context)
+ val best = internalDicts?.let {
+ LocaleUtils.getBestMatch(locale, it.toList()) { dict ->
+ DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dict)
+ }
}
- return userDicts to (best != null)
+ val hasAsset = best != null
+
+ return userDicts to (hasInternalDict || hasAsset)
}
@Preview
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt
index bb9d310e0..cd5c41687 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/GestureTypingScreen.kt
@@ -40,7 +40,7 @@ fun GestureTypingScreen(
val hasGestureLib = JniUtils.sHaveGestureLib
val gestureFloatingPreviewEnabled = prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, Defaults.PREF_GESTURE_FLOATING_PREVIEW_TEXT)
val gestureEnabled = hasGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT)
-
+
// Always show library loader first when no library
val items = buildList {
add(R.string.settings_category_configuration)
@@ -71,14 +71,18 @@ fun GestureTypingScreen(
add(R.string.settings_category_gestures_advanced)
add(Settings.PREF_SPACE_HORIZONTAL_SWIPE)
add(Settings.PREF_SPACE_VERTICAL_SWIPE)
- add(Settings.PREF_TOUCHPAD_SENSITIVITY)
- add(Settings.PREF_TOUCHPAD_EDGE_SCROLL)
+
add(Settings.PREF_DELETE_SWIPE)
add(Settings.PREF_SHORTCUT_ROWS)
if (prefs.getBoolean(Settings.PREF_SHORTCUT_ROWS, Defaults.PREF_SHORTCUT_ROWS)) {
add(Settings.PREF_SHORTCUT_TOP_ROW)
add(Settings.PREF_SHORTCUT_BOTTOM_ROW)
}
+
+ add(R.string.settings_category_touchpad)
+ add(Settings.PREF_TOUCHPAD_SENSITIVITY)
+ add(Settings.PREF_TOUCHPAD_EDGE_SCROLL)
+ add(Settings.PREF_TOUCHPAD_FULLSCREEN)
}
SearchSettingsScreen(
onClickBack = onClickBack,
@@ -170,6 +174,9 @@ fun createGestureTypingSettings(context: Context) = listOf(
description = { value -> value.toInt().toString() }
)
},
+ Setting(context, Settings.PREF_TOUCHPAD_FULLSCREEN, R.string.touchpad_fullscreen, R.string.touchpad_fullscreen_summary) {
+ SwitchPreference(it, Defaults.PREF_TOUCHPAD_FULLSCREEN)
+ },
Setting(context, Settings.PREF_TOUCHPAD_EDGE_SCROLL, R.string.touchpad_edge_scroll, R.string.touchpad_edge_scroll_summary) {
SwitchPreference(it, Defaults.PREF_TOUCHPAD_EDGE_SCROLL)
},
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/LibrariesHubScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/LibrariesHubScreen.kt
index b55410549..80c771936 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/LibrariesHubScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/LibrariesHubScreen.kt
@@ -95,6 +95,17 @@ fun LibrariesHubScreen(
icon = R.drawable.ic_emoji_smileys_emotion
)
+ // Handwriting Input Plugin
+ if (BuildConfig.FLAVOR == "standardfull") {
+ var handwritingInstalled by remember { mutableStateOf(HandwritingLoader.hasPlugin(context)) }
+ LoadHandwritingPluginPreference(
+ title = stringResource(R.string.libraries_hub_handwriting_title),
+ summary = if (handwritingInstalled) stringResource(R.string.libraries_status_active) else stringResource(R.string.libraries_status_not_installed),
+ icon = R.drawable.ic_edit,
+ onSuccess = { handwritingInstalled = HandwritingLoader.hasPlugin(context) }
+ )
+ }
+
// Handwriting Input Plugin
if (BuildConfig.FLAVOR == "standard") {
var handwritingInstalled by remember { mutableStateOf(HandwritingLoader.hasPlugin(context)) }
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/TextExpanderScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/TextExpanderScreen.kt
index 5896fdd11..7c294db68 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/TextExpanderScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/TextExpanderScreen.kt
@@ -71,10 +71,6 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
val context = LocalContext.current
val prefs = context.prefs()
- var prefixText by remember {
- mutableStateOf(TextExpanderUtils.getPrefix(context))
- }
-
var isExpanderEnabled by remember {
mutableStateOf(TextExpanderUtils.isEnabled(context))
}
@@ -90,6 +86,7 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
var isGuideExpanded by remember { mutableStateOf(false) }
var showAddDialog by remember { mutableStateOf(false) }
+ var editingPrefix by remember { mutableStateOf("") }
var editingShortcut by remember { mutableStateOf("") }
var editingTemplate by remember { mutableStateOf(TextFieldValue("")) }
var originalShortcutToEdit by remember { mutableStateOf(null) }
@@ -108,25 +105,27 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
},
filteredItems = { term ->
shortcutsMap.entries
- .filter { (shortcut, template) ->
- val displayShortcut = if (shortcut.startsWith(TextExpanderUtils.REGEX_PREFIX)) {
- shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length)
- } else shortcut
+ .filter { (shortcut, entry) ->
+ val isRegex = shortcut.startsWith(TextExpanderUtils.REGEX_PREFIX)
+ val cleanKey = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
+ val rawShortcut = cleanKey.substring(entry.prefix.length)
+ val displayShortcut = entry.prefix + rawShortcut
displayShortcut.contains(term, ignoreCase = true) ||
- template.contains(term, ignoreCase = true)
+ entry.template.contains(term, ignoreCase = true)
}
.map { Pair(it.key, it.value) }
},
- itemContent = { (shortcut, template) ->
+ itemContent = { (shortcut, entry) ->
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)) {
ShortcutItem(
shortcut = shortcut,
- template = template,
- prefix = prefixText,
+ entry = entry,
onEdit = {
val isRegex = shortcut.startsWith(TextExpanderUtils.REGEX_PREFIX)
- editingShortcut = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
- editingTemplate = TextFieldValue(template)
+ val cleanKey = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
+ editingPrefix = entry.prefix
+ editingShortcut = cleanKey.substring(entry.prefix.length)
+ editingTemplate = TextFieldValue(entry.template)
originalShortcutToEdit = shortcut
editingIsRegex = isRegex
showAddDialog = true
@@ -212,13 +211,13 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
StepBadge(num = "1")
Column(modifier = Modifier.weight(1f)) {
Text(
- text = "Set a Shortcut Prefix",
+ text = "Specify Shortcut Prefix",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
- text = "Choose a prefix like '.' or ';' under prefix configuration to prevent accidental expansions.",
+ text = "Configure an optional prefix like '.' or ';' per shortcut on the edit screen to prevent accidental expansions.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -324,20 +323,17 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
onCheckedChange = { isImmediateEnabled = it }
)
- // 2. Custom Prefix Configuration
- OutlinedTextField(
- value = prefixText,
- onValueChange = {
- prefixText = it
- prefs.edit { putString(TextExpanderUtils.PREF_PREFIX, it) }
- },
- label = { Text("Shortcut Prefix (e.g. '..', '.', ';', or blank)") },
- singleLine = true,
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(16.dp),
- enabled = isExpanderEnabled
+ SwitchPreference(
+ name = "Expand immediately",
+ key = TextExpanderUtils.PREF_IMMEDIATE,
+ default = false,
+ description = "Expand shortcuts immediately without pressing space.",
+ enabled = isExpanderEnabled,
+ onCheckedChange = { isImmediateEnabled = it }
)
+ // global prefix config removed
+
// 3. Section Title / Header for shortcuts
Text(
text = "Custom Shortcuts",
@@ -395,15 +391,16 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
}
}
} else {
- shortcutsMap.forEach { (shortcut, template) ->
+ shortcutsMap.forEach { (shortcut, entry) ->
ShortcutItem(
shortcut = shortcut,
- template = template,
- prefix = prefixText,
+ entry = entry,
onEdit = {
val isRegex = shortcut.startsWith(TextExpanderUtils.REGEX_PREFIX)
- editingShortcut = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
- editingTemplate = TextFieldValue(template)
+ val cleanKey = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
+ editingPrefix = entry.prefix
+ editingShortcut = cleanKey.substring(entry.prefix.length)
+ editingTemplate = TextFieldValue(entry.template)
originalShortcutToEdit = shortcut
editingIsRegex = isRegex
showAddDialog = true
@@ -459,16 +456,16 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
updated.remove(originalShortcutToEdit)
}
val key = if (editingIsRegex) {
- TextExpanderUtils.REGEX_PREFIX + editingShortcut.trim()
+ TextExpanderUtils.REGEX_PREFIX + editingPrefix.trim() + editingShortcut.trim()
} else {
- editingShortcut.trim()
+ editingPrefix.trim() + editingShortcut.trim()
}
- updated[key] = editingTemplate.text
+ updated[key] = TextExpanderUtils.ShortcutEntry(editingTemplate.text, editingPrefix.trim())
shortcutsMap = updated
TextExpanderUtils.saveShortcuts(context, updated)
showAddDialog = false
},
- checkOk = { editingShortcut.trim().isNotEmpty() && editingTemplate.text.isNotEmpty() && isRegexValid },
+ checkOk = { editingShortcut.trim().isNotEmpty() && editingTemplate.text.isNotEmpty() && isRegexValid },
confirmButtonText = if (isEditMode) "Save" else "Add",
neutralButtonText = if (isEditMode) "Delete" else null,
onNeutral = {
@@ -488,15 +485,25 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
focusRequester.requestFocus()
}
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- TextField(
- value = editingShortcut,
- onValueChange = { editingShortcut = if (editingIsRegex) it else it.replace(" ", "") },
- modifier = Modifier
- .fillMaxWidth()
- .focusRequester(focusRequester),
- singleLine = true,
- label = { Text(if (editingIsRegex) "Regex Pattern (e.g. '(\\d+)usd')" else "Shortcut (e.g. 'brb', 'em')") }
- )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ TextField(
+ value = editingPrefix,
+ onValueChange = { editingPrefix = it.replace(" ", "") },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ label = { Text("Prefix (optional)") }
+ )
+ TextField(
+ value = editingShortcut,
+ onValueChange = { editingShortcut = if (editingIsRegex) it else it.replace(" ", "") },
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+ singleLine = true,
+ label = { Text(if (editingIsRegex) "Regex Pattern" else "Shortcut (e.g. 'brb')") }
+ )
+ }
Row(
modifier = Modifier.fillMaxWidth(),
@@ -591,14 +598,14 @@ fun TextExpanderScreen(onClickBack: () -> Unit) {
@Composable
private fun ShortcutItem(
shortcut: String,
- template: String,
- prefix: String,
+ entry: TextExpanderUtils.ShortcutEntry,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
val isRegex = shortcut.startsWith(TextExpanderUtils.REGEX_PREFIX)
- val displayShortcut = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
-
+ val cleanKey = if (isRegex) shortcut.substring(TextExpanderUtils.REGEX_PREFIX.length) else shortcut
+ val rawShortcut = cleanKey.substring(entry.prefix.length)
+ val displayShortcut = entry.prefix + rawShortcut
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
@@ -628,7 +635,7 @@ private fun ShortcutItem(
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
- text = if (isRegex) displayShortcut else "$prefix$displayShortcut",
+ text = displayShortcut,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
@@ -653,11 +660,11 @@ private fun ShortcutItem(
}
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = template,
+ text = entry.template,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
- fontFamily = if (template.contains("%")) androidx.compose.ui.text.font.FontFamily.Monospace else null
+ fontFamily = if (entry.template.contains("%")) androidx.compose.ui.text.font.FontFamily.Monospace else null
)
}
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt
index 29c6e31f6..e57298f1f 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt
@@ -99,8 +99,8 @@ fun createToolbarSettings(context: Context): List {
val filter = { name: String ->
val lowerName = name.lowercase()
when {
- lowerName.startsWith("custom_ai_") -> BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "offline"
- lowerName == "handwriting" -> BuildConfig.FLAVOR == "standard"
+ lowerName.startsWith("custom_ai_") -> BuildConfig.FLAVOR == "standard" || BuildConfig.FLAVOR == "standardfull" || BuildConfig.FLAVOR == "offline"
+ lowerName == "handwriting" -> BuildConfig.FLAVOR == "standardfull"
lowerName in listOf("proofread", "translate", "clipboard_search") -> BuildConfig.FLAVOR != "offlinelite"
else -> true
}
diff --git a/app/src/main/res/drawable/ic_text_edit.xml b/app/src/main/res/drawable/ic_text_edit.xml
new file mode 100644
index 000000000..0b0ab3b28
--- /dev/null
+++ b/app/src/main/res/drawable/ic_text_edit.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/main_keyboard_frame.xml b/app/src/main/res/layout/main_keyboard_frame.xml
index 5ce124c2e..2123eeed6 100644
--- a/app/src/main/res/layout/main_keyboard_frame.xml
+++ b/app/src/main/res/layout/main_keyboard_frame.xml
@@ -43,6 +43,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/touchpad_view.xml b/app/src/main/res/layout/touchpad_view.xml
index 93b67be31..de0ffe6de 100644
--- a/app/src/main/res/layout/touchpad_view.xml
+++ b/app/src/main/res/layout/touchpad_view.xml
@@ -3,7 +3,9 @@
Touchpad mode overlay layout - replaces keyboard with a laptop-style touchpad
SPDX-License-Identifier: GPL-3.0-only
-->
-
+
-
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c60a7a7a4..6f5aa12ee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -70,6 +70,8 @@
Behavior
Advanced Gestures
+
+ Touchpad
Enable split keyboard
@@ -503,6 +505,9 @@
Join next
Force next space
Undo word
+ Text editing
+ Full-screen touchpad
+ Hide suggestion strip and toolbar when touchpad mode is active
Disable learning of new words
diff --git a/app/src/offline/java/helium314/keyboard/latin/utils/ProofreadService.kt b/app/src/offline/java/helium314/keyboard/latin/utils/ProofreadService.kt
index 4e5b178e6..992920ac3 100644
--- a/app/src/offline/java/helium314/keyboard/latin/utils/ProofreadService.kt
+++ b/app/src/offline/java/helium314/keyboard/latin/utils/ProofreadService.kt
@@ -44,160 +44,160 @@ class ProofreadService(private val context: Context) {
// Singleton holder for model state to prevent reloading on every request
object ModelHolder {
- var llamaHelper: LlamaHelper? = null
- var currentModelPath: String? = null
- var isModelAvailable: Boolean = true
- var isModelLoaded: Boolean = false
-
- // Smart Unload Logic
- private var unloadJob: Job? = null
- private val scope = CoroutineScope(kotlinx.coroutines.SupervisorJob() + Dispatchers.IO)
- private const val UNLOAD_DELAY_MS = 10 * 60 * 1000L // 10 minutes
- private val loadMutex = Mutex()
-
- // Flow for LLM events
- val llmFlow = MutableSharedFlow(
- extraBufferCapacity = 64,
- onBufferOverflow = BufferOverflow.DROP_OLDEST
- )
-
- @Synchronized
- fun scheduleUnload(context: Context) {
- unloadJob?.cancel()
-
- val prefs = context.prefs()
- val keepLoaded = prefs.getBoolean(Settings.PREF_OFFLINE_KEEP_MODEL_LOADED, Defaults.PREF_OFFLINE_KEEP_MODEL_LOADED)
-
- if (keepLoaded) {
- Log.i(TAG, "Model unload skipped (Keep Model Loaded enabled)")
- return
- }
-
- unloadJob = scope.launch {
- delay(UNLOAD_DELAY_MS)
- unloadModel()
- Log.i(TAG, "Offline AI model unloaded due to inactivity")
- }
- }
-
- @Synchronized
- fun cancelUnload() {
- unloadJob?.cancel()
- unloadJob = null
+var llamaHelper: LlamaHelper? = null
+var currentModelPath: String? = null
+var isModelAvailable: Boolean = true
+var isModelLoaded: Boolean = false
+
+// Smart Unload Logic
+private var unloadJob: Job? = null
+private val scope = CoroutineScope(kotlinx.coroutines.SupervisorJob() + Dispatchers.IO)
+private const val UNLOAD_DELAY_MS = 10 * 60 * 1000L // 10 minutes
+private val loadMutex = Mutex()
+
+// Flow for LLM events
+val llmFlow = MutableSharedFlow(
+ extraBufferCapacity = 64,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+)
+
+@Synchronized
+fun scheduleUnload(context: Context) {
+ unloadJob?.cancel()
+
+ val prefs = context.prefs()
+ val keepLoaded = prefs.getBoolean(Settings.PREF_OFFLINE_KEEP_MODEL_LOADED, Defaults.PREF_OFFLINE_KEEP_MODEL_LOADED)
+
+ if (keepLoaded) {
+ Log.i(TAG, "Model unload skipped (Keep Model Loaded enabled)")
+ return
}
- @Synchronized
- fun unloadModel() {
- try {
- llamaHelper?.release()
- } catch (e: Exception) {
- Log.w(TAG, "Error unloading llama model", e)
- }
- llamaHelper = null
- currentModelPath = null
- isModelLoaded = false
- isModelAvailable = true
+ unloadJob = scope.launch {
+ delay(UNLOAD_DELAY_MS)
+ unloadModel()
+ Log.i(TAG, "Offline AI model unloaded due to inactivity")
}
+}
- suspend fun loadModel(
- context: Context,
- modelPath: String
- ): Boolean = loadMutex.withLock {
- cancelUnload()
-
- // Check if already loaded with same path
- if (isModelLoaded && currentModelPath == modelPath && llamaHelper != null) {
- return true
- }
-
- unloadModel() // Ensure clean slate if path changed
-
- return try {
- val contentResolver = context.contentResolver
- val helper = LlamaHelper(
- contentResolver,
- scope,
- llmFlow
- )
-
- // Get llama via reflection
- val llamaField = LlamaHelper::class.java.getDeclaredField("llama\$delegate").apply { isAccessible = true }
- val llamaLazy = llamaField.get(helper) as Lazy
- val llama = llamaLazy.value
-
- // Detach model file descriptor
- val uri = android.net.Uri.parse(modelPath)
- val pfd = contentResolver.openFileDescriptor(uri, "r")
- ?: throw IllegalArgumentException("Failed to open model file descriptor")
- val modelFd = pfd.detachFd()
+@Synchronized
+fun cancelUnload() {
+ unloadJob?.cancel()
+ unloadJob = null
+}
- // Calculate optimal threads count (4 threads is the sweet spot for mobile CPUs)
- val cores = Runtime.getRuntime().availableProcessors()
- val threads = if (cores <= 4) cores else 4
+@Synchronized
+fun unloadModel() {
+ try {
+ llamaHelper?.release()
+ } catch (e: Exception) {
+ Log.w(TAG, "Error unloading llama model", e)
+ }
+ llamaHelper = null
+ currentModelPath = null
+ isModelLoaded = false
+ isModelAvailable = true
+}
- Log.i(TAG, "Loading GGUF model: threads=$threads (cores=$cores), use_mmap=false")
+suspend fun loadModel(
+ context: Context,
+ modelPath: String
+): Boolean = loadMutex.withLock {
+ cancelUnload()
- // Construct parameters map
- val params = mutableMapOf(
- "model" to modelPath,
- "model_fd" to modelFd,
- "use_mmap" to false,
- "use_mlock" to false,
- "n_ctx" to 2048,
- "embedding" to false,
- "n_batch" to 512,
- "n_threads" to threads,
- "n_gpu_layers" to 0,
- "vocab_only" to false,
- "lora" to "",
- "lora_scaled" to 1.0,
- "rope_freq_base" to 0.0,
- "rope_freq_scale" to 0.0
- )
+ // Check if already loaded with same path
+ if (isModelLoaded && currentModelPath == modelPath && llamaHelper != null) {
+ return true
+ }
- // JNI callback called by native code for each token
- val callback: (String) -> Unit = { word ->
- try {
- val allTextField = LlamaHelper::class.java.getDeclaredField("allText").apply { isAccessible = true }
- val currentAllText = allTextField.get(helper) as String
- allTextField.set(helper, currentAllText + word)
+ unloadModel() // Ensure clean slate if path changed
+
+ return try {
+ val contentResolver = context.contentResolver
+ val helper = LlamaHelper(
+ contentResolver,
+ scope,
+ llmFlow
+ )
+
+ // Get llama via reflection
+ val llamaField = LlamaHelper::class.java.getDeclaredField("llama\$delegate").apply { isAccessible = true }
+ val llamaLazy = llamaField.get(helper) as Lazy
+ val llama = llamaLazy.value
+
+ // Detach model file descriptor
+ val uri = android.net.Uri.parse(modelPath)
+ val pfd = contentResolver.openFileDescriptor(uri, "r")
+ ?: throw IllegalArgumentException("Failed to open model file descriptor")
+ val modelFd = pfd.detachFd()
+
+ // Calculate optimal threads count (4 threads is the sweet spot for mobile CPUs)
+ val cores = Runtime.getRuntime().availableProcessors()
+ val threads = if (cores <= 4) cores else 4
+
+ Log.i(TAG, "Loading GGUF model: threads=$threads (cores=$cores), use_mmap=false")
+
+ // Construct parameters map
+ val params = mutableMapOf(
+ "model" to modelPath,
+ "model_fd" to modelFd,
+ "use_mmap" to false,
+ "use_mlock" to false,
+ "n_ctx" to 2048,
+ "embedding" to false,
+ "n_batch" to 512,
+ "n_threads" to threads,
+ "n_gpu_layers" to 0,
+ "vocab_only" to false,
+ "lora" to "",
+ "lora_scaled" to 1.0,
+ "rope_freq_base" to 0.0,
+ "rope_freq_scale" to 0.0
+ )
+
+ // JNI callback called by native code for each token
+ val callback: (String) -> Unit = { word ->
+ try {
+ val allTextField = LlamaHelper::class.java.getDeclaredField("allText").apply { isAccessible = true }
+ val currentAllText = allTextField.get(helper) as String
+ allTextField.set(helper, currentAllText + word)
- val tokenCountField = LlamaHelper::class.java.getDeclaredField("tokenCount").apply { isAccessible = true }
- val currentCount = tokenCountField.get(helper) as Int
- tokenCountField.set(helper, currentCount + 1)
+ val tokenCountField = LlamaHelper::class.java.getDeclaredField("tokenCount").apply { isAccessible = true }
+ val currentCount = tokenCountField.get(helper) as Int
+ tokenCountField.set(helper, currentCount + 1)
- helper.sharedFlow.tryEmit(LlamaHelper.LLMEvent.Ongoing(word, currentCount + 1))
- } catch (e: Throwable) {
- Log.e(TAG, "Error in native token callback", e)
- }
+ helper.sharedFlow.tryEmit(LlamaHelper.LLMEvent.Ongoing(word, currentCount + 1))
+ } catch (e: Throwable) {
+ Log.e(TAG, "Error in native token callback", e)
}
+ }
- // Start the engine
- val result = llama.startEngine(params, callback)
+ // Start the engine
+ val result = llama.startEngine(params, callback)
- val contextId = result?.get("contextId") as? Int
- ?: throw IllegalStateException("contextId not found in result map")
+ val contextId = result?.get("contextId") as? Int
+ ?: throw IllegalStateException("contextId not found in result map")
- // Set currentContext via reflection
- val currentContextField = LlamaHelper::class.java.getDeclaredField("currentContext").apply { isAccessible = true }
- currentContextField.set(helper, contextId)
+ // Set currentContext via reflection
+ val currentContextField = LlamaHelper::class.java.getDeclaredField("currentContext").apply { isAccessible = true }
+ currentContextField.set(helper, contextId)
- // Emit Loaded event
- helper.sharedFlow.tryEmit(LlamaHelper.LLMEvent.Loaded(modelPath))
+ // Emit Loaded event
+ helper.sharedFlow.tryEmit(LlamaHelper.LLMEvent.Loaded(modelPath))
- llamaHelper = helper
- currentModelPath = modelPath
- isModelLoaded = true
- isModelAvailable = true
- true
- } catch (e: Throwable) {
- Log.e(TAG, "Failed to load GGUF model", e)
- isModelAvailable = false
- false
- }
+ llamaHelper = helper
+ currentModelPath = modelPath
+ isModelLoaded = true
+ isModelAvailable = true
+ true
+ } catch (e: Throwable) {
+ Log.e(TAG, "Failed to load GGUF model", e)
+ isModelAvailable = false
+ false
}
+}
- private const val TAG = "LlamaProofreadService"
+private const val TAG = "LlamaProofreadService"
}
// AI Provider support (API compatibility)
diff --git a/app/src/offlinelite/java/helium314/keyboard/latin/utils/ProofreadService.kt b/app/src/offlinelite/java/helium314/keyboard/latin/utils/ProofreadService.kt
index 062d2dc46..37b43f6c4 100644
--- a/app/src/offlinelite/java/helium314/keyboard/latin/utils/ProofreadService.kt
+++ b/app/src/offlinelite/java/helium314/keyboard/latin/utils/ProofreadService.kt
@@ -19,6 +19,8 @@ class ProofreadService(private val context: Context) {
+
+
val prefs: SharedPreferences get() = context.prefs()
// Always returns GEMINI as default, but methods do nothing
diff --git a/app/src/standard/java/helium314/keyboard/latin/utils/ProofreadService.kt b/app/src/standard/java/helium314/keyboard/latin/utils/ProofreadService.kt
index 010ba170f..54ced1e37 100644
--- a/app/src/standard/java/helium314/keyboard/latin/utils/ProofreadService.kt
+++ b/app/src/standard/java/helium314/keyboard/latin/utils/ProofreadService.kt
@@ -65,9 +65,9 @@ class ProofreadService(private val context: Context) {
fun getProvider(): AIProvider {
val providerStr = context.prefs().getString(KEY_PROVIDER, AIProvider.GEMINI.name)
return try {
- AIProvider.valueOf(providerStr ?: AIProvider.GEMINI.name)
+AIProvider.valueOf(providerStr ?: AIProvider.GEMINI.name)
} catch (e: IllegalArgumentException) {
- AIProvider.GEMINI
+AIProvider.GEMINI
}
}
diff --git a/app/src/standardfull/AndroidManifest.xml b/app/src/standardfull/AndroidManifest.xml
new file mode 100644
index 000000000..90802b351
--- /dev/null
+++ b/app/src/standardfull/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/badges/download.svg b/docs/badges/download.svg
new file mode 100644
index 000000000..cb82e0b80
--- /dev/null
+++ b/docs/badges/download.svg
@@ -0,0 +1 @@
+
diff --git a/docs/badges/downloads.svg b/docs/badges/downloads.svg
new file mode 100644
index 000000000..720b811be
--- /dev/null
+++ b/docs/badges/downloads.svg
@@ -0,0 +1 @@
+
diff --git a/docs/badges/stars.svg b/docs/badges/stars.svg
new file mode 100644
index 000000000..74a51bf4d
--- /dev/null
+++ b/docs/badges/stars.svg
@@ -0,0 +1 @@
+
diff --git a/docs/releasenote/release_notes_v3.8.9.md b/docs/releasenote/release_notes_v3.8.9.md
new file mode 100644
index 000000000..dfa8a5003
--- /dev/null
+++ b/docs/releasenote/release_notes_v3.8.9.md
@@ -0,0 +1,39 @@
+### 💖 Support Our Work
+* We are committed to making our apps as powerful and polished as possible. As an entirely community-funded project, we rely on your support to keep going, please consider becoming a [sponsor](https://github.com/sponsors/LeanBitLab). A huge thank you to all our current supporters!
+
+## 🚀 What's New
+
+### 🖱️ Touchpad & Text Editing Mode
+- **Gboard-style Text Editing Mode**: Added a dedicated cursor navigation and editing overlay. Activated from the toolbar, it offers precise DPAD navigation, custom selection mode (Shift + arrows), clipboard actions (Select All, Copy, Cut, Paste), and editing utilities (Backspace, Forward Delete).
+- **Dedicated Touchpad settings section**: Extracted Touchpad configuration from Advanced Gestures into its own dedicated settings category page.
+- **Full-screen Touchpad**: Added a preference to automatically hide the suggestion strip and toolbar when Touchpad mode is active, maximizing the touchpad surface area.
+- **Touchpad Close Button**: Added an overlay close button at the bottom-right corner of the touchpad, styled dynamically according to the active keyboard theme, for quick exit.
+
+### 📋 Clipboard & Screenshot Suggestions
+- **Persistent Suggestions Dismissal**: The keyboard now remembers if you dismissed a clipboard text or screenshot suggestion, preventing it from showing up again even across keyboard restarts. The dismissed state automatically resets when a new item is copied.
+- **Reduced Suggestion Timeouts**: Reduced clipboard and screenshot suggestion timeouts to 1 minute to keep suggestions fresh.
+
+### 🔤 Text Expander Improvements
+- **Shortcut Prefix Fix**: Resolved a prefix matching bug where custom prefixes (e.g. `*` for shortcut `g`) were prepended twice (matching `**g` instead of `*g`), restoring instant and spacebar text expansion.
+- **Improved Dialog Layout**: Stacked the Prefix and Shortcut input fields vertically inside the Add/Edit Shortcut dialog for better legibility.
+
+### 📚 Dictionary & Settings Refinement
+- **Polished Dictionary Settings UI**: Re-designed the layout using modern Material 3 Card components, colorful badges/chips for dictionary types, and uniform utility icon sizes.
+- **Smart Language Fallback**: Regional variants (e.g., `ml-IN`) will now fall back to the general parent language dictionary (e.g., `ml`) if country-specific files are missing, ensuring seamless next-word predictions.
+- **Unified Dictionary Toggling**: Grouped all dictionary subtypes (including downloaded emoji dictionaries) under a single master toggle per language card, and removed redundant buttons.
+- **Aesthetic Filtering**: Hidden non-enabled regional variant cards from the settings menu to reduce clutter, while keeping them configurable inside the active language dialog.
+- **Download Path Collision Fix**: Extracted specific locales from download URLs to prevent file collision and duplicate settings cards.
+
+### ⚙️ Engine & Compatibility
+- **Build Flavor Split**: Split standard build into FOSS-compliant "standard" (no ML Kit, F-Droid ready) and "standardfull" (includes ML Kit/Handwriting).
+- **Secure Signing Workflow**: Added secure CI/CD release build signing workflow for reproducible builds.
+- **ABI & Theme Fixes**: Re-added `armeabi-v7a` support to ABI filters, fixed popup key contrast-aware colors on AMOLED black theme, and improved split mode suggestions.
+
+## 📦 Downloads (Choose Your Flavor)
+
+| File | Description | Permissions |
+| :--- | :--- | :--- |
+| **`1-LeanType_3.8.9-standardfull-release.apk`** | **Recommended**. Cloud AI | Internet |
+| **`2-LeanType_3.8.9-standard-release.apk`** | **Fdroid Build**. Standard + No Handwrite | Internet |
+| **`3-LeanType_3.8.9-offline-release.apk`** | **Privacy Focused**. No Internet. Offline AI Only. | No Internet |
+| **`4-LeanType_3.8.9-offlinelite-release.apk`** | **Minimalist**. Pure FOSS. No AI code. | No Internet |
diff --git a/docs/scripts/generate_release_notes.py b/docs/scripts/generate_release_notes.py
new file mode 100755
index 000000000..94cc57680
--- /dev/null
+++ b/docs/scripts/generate_release_notes.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+import os
+import re
+
+def main():
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ project_root = os.path.dirname(os.path.dirname(script_dir))
+
+ # 1. Try to get version from tag name (if running in GitHub Actions)
+ ref_name = os.environ.get('GITHUB_REF_NAME')
+ version_name = None
+ if ref_name and ref_name.startswith('v'):
+ version_name = ref_name[1:]
+ print(f"Detected version name from GITHUB_REF_NAME: {version_name}")
+
+ # 2. Fall back to build.gradle.kts if not running in action or tag not matching
+ if not version_name:
+ gradle_path = os.path.join(project_root, 'app', 'build.gradle.kts')
+ if os.path.exists(gradle_path):
+ with open(gradle_path, 'r', encoding='utf-8') as f:
+ gradle_content = f.read()
+ version_name_match = re.search(r'versionName\s*=\s*"([^"]+)"', gradle_content)
+ if version_name_match:
+ version_name = version_name_match.group(1)
+ print(f"Parsed version name from build.gradle.kts: {version_name}")
+
+ if not version_name:
+ print("Error: Could not determine version name")
+ return
+
+ # 3. Locate the existing release notes file
+ releasenote_dir = os.path.join(project_root, 'docs', 'releasenote')
+ source_path = os.path.join(releasenote_dir, f'release_notes_v{version_name}.md')
+ temp_path = os.path.join(releasenote_dir, 'release_notes_temp.md')
+
+ if not os.path.exists(source_path):
+ print(f"Error: Release note file {source_path} not found")
+ # Write a fallback file so the build/release step doesn't fail
+ with open(temp_path, 'w', encoding='utf-8') as f:
+ f.write(f"Release notes for version {version_name}")
+ return
+
+ # 4. Copy to release_notes_temp.md
+ with open(source_path, 'r', encoding='utf-8') as sf:
+ content = sf.read()
+
+ with open(temp_path, 'w', encoding='utf-8') as df:
+ df.write(content)
+
+ print(f"Successfully copied {source_path} to {temp_path}")
+
+if __name__ == '__main__':
+ # ponytail: copy pre-generated release notes file to temp.md
+ main()
diff --git a/fastlane/metadata/android/en-US/changelogs/3890.txt b/fastlane/metadata/android/en-US/changelogs/3890.txt
new file mode 100644
index 000000000..dacad3b08
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3890.txt
@@ -0,0 +1,7 @@
+- Gboard-style Text Editing panel with cursor & selection controls
+- Dedicated Touchpad settings, Full-screen mode & Close button
+- Persistent clipboard/screenshot dismiss & 1-minute timeout
+- Text expander shortcut prefix fix & layout updates
+- Polished Dictionary settings UI with Material 3 cards
+- Prevent dictionary download path clashes & support fallback locales
+- Unified dictionary toggles & regional cards filtering