diff --git a/app/src/main/assets/layouts/editing/editing.json b/app/src/main/assets/layouts/editing/editing.json new file mode 100644 index 000000000..b750b92da --- /dev/null +++ b/app/src/main/assets/layouts/editing/editing.json @@ -0,0 +1,30 @@ +[ + [ + { "code": -131, "label": "Undo", "type": "function", "width": 0.2 }, + { "code": -132, "label": "Redo", "type": "function", "width": 0.2 }, + { "code": -35, "label": "All", "type": "function", "width": 0.2 }, + { "code": -34, "label": "Word", "type": "function", "width": 0.2 }, + { "code": -201, "label": "✕", "type": "function", "width": 0.2 } + ], + [ + { "code": -32, "label": "Cut", "type": "function", "width": 0.2 }, + { "code": -25, "label": "⤒", "width": 0.2 }, + { "code": -23, "label": "↑", "width": 0.2 }, + { "code": -26, "label": "⤓", "width": 0.2 }, + { "code": -7, "label": "delete", "type": "function", "width": 0.2 } + ], + [ + { "code": -31, "label": "Copy", "type": "function", "width": 0.2 }, + { "code": -21, "label": "←", "width": 0.2 }, + { "code": -306, "label": "Select", "type": "function", "width": 0.2 }, + { "code": -22, "label": "→", "width": 0.2 }, + { "code": -10015, "label": "«", "width": 0.2 } + ], + [ + { "code": -33, "label": "Paste", "type": "function", "width": 0.2 }, + { "code": -27, "label": "⇱", "width": 0.2 }, + { "code": -24, "label": "↓", "width": 0.2 }, + { "code": -28, "label": "⇲", "width": 0.2 }, + { "code": -10016, "label": "»", "width": 0.2 } + ] +] diff --git a/app/src/main/java/com/android/inputmethod/latin/BinaryDictionary.java b/app/src/main/java/com/android/inputmethod/latin/BinaryDictionary.java index a8fd5ad1f..159266d27 100644 --- a/app/src/main/java/com/android/inputmethod/latin/BinaryDictionary.java +++ b/app/src/main/java/com/android/inputmethod/latin/BinaryDictionary.java @@ -429,6 +429,37 @@ public GetNextWordPropertyResult getNextWordProperty(final int token) { getWordProperty(word, isBeginningOfSentence[0]), nextToken); } + // ponytail: allocation-free container for fast dict iteration + public static class WordAndFrequency { + public final String mWord; + public final int mFrequency; + public WordAndFrequency(String word, int frequency) { + mWord = word; + mFrequency = frequency; + } + } + + // ponytail: allocation-free result container for fast dict iteration + public static class GetNextWordAndFrequencyResult { + public final WordAndFrequency mWordAndFrequency; + public final int mNextToken; + public GetNextWordAndFrequencyResult(WordAndFrequency wordAndFrequency, int nextToken) { + mWordAndFrequency = wordAndFrequency; + mNextToken = nextToken; + } + } + + // ponytail: fast word iteration that avoids fetching heavy shortcut/ngram metadata + public GetNextWordAndFrequencyResult getNextWordAndFrequency(final int token) { + final int[] codePoints = new int[DICTIONARY_MAX_WORD_LENGTH]; + final boolean[] isBeginningOfSentence = new boolean[1]; + final int nextToken = getNextWordNative(mNativeDict, token, codePoints, + isBeginningOfSentence); + final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); + final int probability = getFrequency(word); + return new GetNextWordAndFrequencyResult(new WordAndFrequency(word, probability), nextToken); + } + // Add a unigram entry to binary dictionary with unigram attributes in native code. public boolean addUnigramEntry(final String word, final int probability, final String shortcutTarget, final int shortcutProbability, diff --git a/app/src/main/java/helium314/keyboard/keyboard/Key.java b/app/src/main/java/helium314/keyboard/keyboard/Key.java index 8d983c2de..43239355c 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/Key.java +++ b/app/src/main/java/helium314/keyboard/keyboard/Key.java @@ -1308,6 +1308,7 @@ public KeyParams( // action flags don't need to be specified, they can be deduced from the key if (mCode == Constants.CODE_SPACE || mCode == KeyCode.LANGUAGE_SWITCH + || mCode == KeyCode.CLEAR_HANDWRITING || (mCode == KeyCode.SYMBOL_ALPHA && !params.mId.isAlphabetKeyboard())) actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; if (mCode <= Constants.CODE_SPACE && mCode != KeyCode.MULTIPLE_CODE_POINTS && mIconName == null) diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt index d6134485a..4d6e7bd38 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt @@ -102,7 +102,39 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp } override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) { + val isArrow = primaryCode == KeyCode.ARROW_LEFT || primaryCode == KeyCode.ARROW_RIGHT || primaryCode == KeyCode.ARROW_UP || primaryCode == KeyCode.ARROW_DOWN + if (isArrow) { + val isSelecting = keyboardSwitcher.keyboard?.mId?.isAlphabetShifted == true || sPersistentSelectionModeActive + if (isSelecting) { + val androidKeyCode = when (primaryCode) { + KeyCode.ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT + KeyCode.ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT + KeyCode.ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP + KeyCode.ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN + 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)) + } + return + } + } when (primaryCode) { + KeyCode.TOGGLE_SELECTION_MODE -> { + sPersistentSelectionModeActive = !sPersistentSelectionModeActive + keyboardSwitcher.mainKeyboardView?.invalidateAllKeys() + keyboardSwitcher.suggestionStripView?.refreshToolbarButtonsActivation() + return + } + KeyCode.ALPHA -> { + sPersistentTextEditModeActive = false + sPersistentSelectionModeActive = false + keyboardSwitcher.hideTextEditView() + } KeyCode.HANDWRITING -> { if (keyboardSwitcher.isHandwritingShowing) { keyboardSwitcher.setAlphabetKeyboard() @@ -167,12 +199,7 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp if (sPersistentTextEditModeActive) { PointerTracker.sPersistentTouchpadModeActive = false keyboardSwitcher.hideTouchpadView() - - val textEditView = keyboardSwitcher.textEditView - if (textEditView != null) { - setupTextEditListener(textEditView) - keyboardSwitcher.showTextEditView() - } + keyboardSwitcher.showTextEditView() } else { keyboardSwitcher.hideTextEditView() } @@ -188,17 +215,21 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp } val mkv = keyboardSwitcher.mainKeyboardView + val isEditingNav = primaryCode == KeyCode.WORD_LEFT || primaryCode == KeyCode.WORD_RIGHT + || primaryCode == KeyCode.MOVE_START_OF_PAGE || primaryCode == KeyCode.MOVE_END_OF_PAGE + || primaryCode == KeyCode.MOVE_START_OF_LINE || primaryCode == KeyCode.MOVE_END_OF_LINE + || primaryCode == KeyCode.PAGE_UP || primaryCode == KeyCode.PAGE_DOWN + val eventMetaState = if (isEditingNav && (keyboardSwitcher.keyboard?.mId?.isAlphabetShifted == true || sPersistentSelectionModeActive)) { + metaState or KeyEvent.META_SHIFT_ON + } else { + metaState + } + // checking if the character is a combining accent val event = if (primaryCode in combiningRange) { // todo: should this be done later, maybe in inputLogic? - Event.createSoftwareDeadEvent(primaryCode, 0, metaState, mkv.getKeyX(x), mkv.getKeyY(y), null) + Event.createSoftwareDeadEvent(primaryCode, 0, eventMetaState, mkv.getKeyX(x), mkv.getKeyY(y), null) } else { - // todo: - // setting meta shift should only be done for arrow and similar cursor movement keys - // should only be enabled once it works more reliably (currently depends on app for some reason) -// if (mkv.keyboard?.mId?.isAlphabetShiftedManually == true) -// Event.createSoftwareKeypressEvent(primaryCode, metaState or KeyEvent.META_SHIFT_ON, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat) -// else Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat) - Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat) + Event.createSoftwareKeypressEvent(primaryCode, eventMetaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat) } latinIME.onEvent(event) metaAfterCodeInput(primaryCode) @@ -360,6 +391,21 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp val rtl = RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype val steps = if (rtl) -rawSteps else rawSteps + val isSelecting = keyboardSwitcher.keyboard?.mId?.isAlphabetShifted == true || sPersistentSelectionModeActive + if (isSelecting) { + val code = if (steps < 0) { + gestureMoveBackHaptics() + if (rtl) KeyCode.ARROW_RIGHT else KeyCode.ARROW_LEFT + } else { + gestureMoveForwardHaptics(true) + if (rtl) KeyCode.ARROW_LEFT else KeyCode.ARROW_RIGHT + } + repeat(abs(steps)) { + onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) + } + return true + } + // Web editors (Chromium, Firefox, etc.) handle direct setSelection badly during fast swipes, // often resulting in focus loss, caret hiding, or composition desynchronization. // Fall back to sending simulated arrow keys, which is fast, asynchronous, and robust. @@ -670,43 +716,13 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp }) } - 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 + @JvmField + var sPersistentSelectionModeActive = 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/KeyboardId.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java index 7429f5d01..3276af6f5 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java @@ -68,6 +68,7 @@ public final class KeyboardId { public static final int ELEMENT_EMOJI_BOTTOM_ROW = 29; public static final int ELEMENT_CLIPBOARD_BOTTOM_ROW = 30; public static final int ELEMENT_HANDWRITING_BOTTOM_ROW = 31; + public static final int ELEMENT_TEXT_EDIT = 32; public final RichInputMethodSubtype mSubtype; public final int mWidth; @@ -294,6 +295,7 @@ public static String elementIdToName(final int elementId) { case ELEMENT_EMOJI_BOTTOM_ROW -> "emojiBottomRow"; case ELEMENT_CLIPBOARD_BOTTOM_ROW -> "clipboardBottomRow"; case ELEMENT_HANDWRITING_BOTTOM_ROW -> "handwritingBottomRow"; + case ELEMENT_TEXT_EDIT -> "editing"; default -> null; }; } diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java index 1060da20f..9f9334369 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java @@ -72,7 +72,6 @@ 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; @@ -221,7 +220,17 @@ private void setKeyboard(final int keyboardId, @NonNull final KeyboardSwitchStat // TODO: pass this object to setKeyboard instead of getting the current values. final MainKeyboardView keyboardView = mKeyboardView; final Keyboard oldKeyboard = keyboardView.getKeyboard(); - final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(keyboardId); + final int targetId; + if (KeyboardActionListenerImpl.sPersistentTextEditModeActive && (keyboardId == KeyboardId.ELEMENT_ALPHABET + || keyboardId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED + || keyboardId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED + || keyboardId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED + || keyboardId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)) { + targetId = KeyboardId.ELEMENT_TEXT_EDIT; + } else { + targetId = keyboardId; + } + final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(targetId); keyboardView.setKeyboard(newKeyboard); mCurrentInputView.setKeyboardTopPadding(newKeyboard.mTopPadding); keyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn); @@ -344,7 +353,7 @@ private void setMainKeyboardFrame( final int stripVisibility = settingsValues.mToolbarMode == ToolbarMode.HIDDEN ? View.GONE : View.VISIBLE; mStripContainer.setVisibility(stripVisibility); PointerTracker.switchTo(mKeyboardView); - if (PointerTracker.sPersistentTouchpadModeActive || KeyboardActionListenerImpl.sPersistentTextEditModeActive) { + if (PointerTracker.sPersistentTouchpadModeActive) { mKeyboardView.setVisibility(visibility == View.VISIBLE ? View.INVISIBLE : View.GONE); } else { mKeyboardView.setVisibility(visibility); @@ -383,21 +392,11 @@ 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); - } + private static void clearTextEditModeState() { + KeyboardActionListenerImpl.sPersistentTextEditModeActive = false; + KeyboardActionListenerImpl.sPersistentSelectionModeActive = false; } // Implements {@link KeyboardState.SwitchActions}. @@ -410,10 +409,7 @@ public void setEmojiKeyboard() { if (mTouchpadView != null) { mTouchpadView.setVisibility(View.GONE); } - KeyboardActionListenerImpl.sPersistentTextEditModeActive = false; - if (mTextEditView != null) { - mTextEditView.setVisibility(View.GONE); - } + clearTextEditModeState(); mMainKeyboardFrame.setVisibility(View.VISIBLE); // The visibility of {@link #mKeyboardView} must be aligned with {@link // #MainKeyboardFrame}. @@ -442,10 +438,7 @@ public void setClipboardKeyboard() { if (mTouchpadView != null) { mTouchpadView.setVisibility(View.GONE); } - KeyboardActionListenerImpl.sPersistentTextEditModeActive = false; - if (mTextEditView != null) { - mTextEditView.setVisibility(View.GONE); - } + clearTextEditModeState(); mMainKeyboardFrame.setVisibility(View.VISIBLE); // The visibility of {@link #mKeyboardView} must be aligned with {@link // #MainKeyboardFrame}. @@ -473,10 +466,7 @@ public void setHandwritingKeyboard() { if (mTouchpadView != null) { mTouchpadView.setVisibility(View.GONE); } - KeyboardActionListenerImpl.sPersistentTextEditModeActive = false; - if (mTextEditView != null) { - mTextEditView.setVisibility(View.GONE); - } + clearTextEditModeState(); mMainKeyboardFrame.setVisibility(View.VISIBLE); mKeyboardView.setVisibility(View.GONE); mEmojiTabStripView.setVisibility(View.GONE); @@ -694,39 +684,15 @@ public TouchpadView getTouchpadView() { } 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); + setKeyboard(KeyboardId.ELEMENT_TEXT_EDIT, KeyboardSwitchState.OTHER); } 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); - } + clearTextEditModeState(); + setAlphabetKeyboard(); } - public TextEditView getTextEditView() { - return mTextEditView; - } + public void toggleSplitKeyboardMode() { final Settings settings = Settings.getInstance(); @@ -890,8 +856,6 @@ public View getVisibleKeyboardView() { return mHandwritingView; } else if (mTouchpadView != null && mTouchpadView.isShown()) { return mTouchpadView; - } else if (mTextEditView != null && mTextEditView.isShown()) { - return mTextEditView; } return mKeyboardView; } @@ -992,13 +956,6 @@ 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( @@ -1008,14 +965,6 @@ 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/KeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java index 695df5996..ae90c4ea4 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java @@ -80,6 +80,7 @@ public class KeyboardView extends View { private final KeyDrawParams mKeyDrawParams = new KeyDrawParams(); // Drawing + private final java.util.Map mKeyCustomBgColors = new java.util.HashMap<>(); /** True if all keys should be drawn */ private boolean mInvalidateAllKeys; /** The keys that should be drawn */ @@ -182,12 +183,13 @@ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { * Attaches a keyboard to this view. The keyboard can be switched at any time * and the * view will re-layout itself to accommodate the keyboard. - * + * * @see Keyboard * @see #getKeyboard() * @param keyboard the keyboard to display in this view */ public void setKeyboard(@NonNull final Keyboard keyboard) { + mKeyCustomBgColors.clear(); if (keyboard instanceof MoreSuggestions) { mColors.setBackground(this, ColorType.MORE_SUGGESTIONS_BACKGROUND); } else if (keyboard instanceof PopupKeysKeyboard) { @@ -217,7 +219,7 @@ public void setKeyboard(@NonNull final Keyboard keyboard) { /** * Returns the current keyboard being displayed by this view. - * + * * @return the currently attached keyboard * @see #setKeyboard(Keyboard) */ @@ -391,21 +393,80 @@ protected void onDrawKeyBackground(@NonNull final Key key, @NonNull final Canvas } background.setBounds(0, 0, bgWidth, bgHeight); canvas.translate(bgX, bgY); - - final boolean isShiftLocked = key.getCode() == KeyCode.SHIFT && key.isLocked(); - if (isShiftLocked) { + + final boolean isSelected = (key.getCode() == KeyCode.SHIFT && key.isLocked()) + || (key.getCode() == KeyCode.TOGGLE_SELECTION_MODE && KeyboardActionListenerImpl.sPersistentSelectionModeActive); + + boolean hasCustomTint = false; + if (KeyboardActionListenerImpl.sPersistentTextEditModeActive) { + int customColor = 0; + switch (key.getCode()) { + case KeyCode.UNDO: + case KeyCode.REDO: + case KeyCode.DELETE: + customColor = mColors.get(ColorType.EDIT_MODE_DELETE_BACKGROUND); + break; + case KeyCode.CLIPBOARD_SELECT_ALL: + case KeyCode.CLIPBOARD_SELECT_WORD: + case KeyCode.TOGGLE_SELECTION_MODE: + case KeyCode.CLIPBOARD_CUT: + case KeyCode.CLIPBOARD_COPY: + case KeyCode.CLIPBOARD_PASTE: + customColor = mColors.get(ColorType.EDIT_MODE_FUNC_BACKGROUND); + break; + case KeyCode.ALPHA: + customColor = mColors.get(ColorType.EDIT_MODE_ALPHA_BACKGROUND); + break; + case KeyCode.ARROW_UP: + case KeyCode.ARROW_DOWN: + case KeyCode.ARROW_LEFT: + case KeyCode.ARROW_RIGHT: + case 32: // space + customColor = mColors.get(ColorType.EDIT_MODE_NAV_BACKGROUND); + break; + case KeyCode.MOVE_START_OF_PAGE: + case KeyCode.MOVE_END_OF_PAGE: + case KeyCode.MOVE_START_OF_LINE: + case KeyCode.MOVE_END_OF_LINE: + case KeyCode.WORD_LEFT: + case KeyCode.WORD_RIGHT: + customColor = mColors.get(ColorType.EDIT_MODE_JUMP_BACKGROUND); + break; + } + if (customColor != 0) { + androidx.core.graphics.drawable.DrawableCompat.setTint(background, customColor); + mKeyCustomBgColors.put(key, customColor); + hasCustomTint = true; + } + } + + if (isSelected) { background.setColorFilter(Color.argb(0x80, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); } - + background.draw(canvas); - - if (isShiftLocked) { + + if (isSelected) { background.clearColorFilter(); } - + if (hasCustomTint) { + final ColorType originalType = key.getBackgroundType() == Key.BACKGROUND_TYPE_FUNCTIONAL + ? ColorType.FUNCTIONAL_KEY_BACKGROUND + : ColorType.KEY_BACKGROUND; + mColors.setColor(background, originalType); + } + canvas.translate(-bgX, -bgY); } + private static int blend(int c1, int c2, float ratio) { + float inverseRatio = 1f - ratio; + float r = Color.red(c1) * ratio + Color.red(c2) * inverseRatio; + float g = Color.green(c1) * ratio + Color.green(c2) * inverseRatio; + float b = Color.blue(c1) * ratio + Color.blue(c2) * inverseRatio; + return Color.rgb((int) r, (int) g, (int) b); + } + // Draw key top visuals. protected void onDrawKeyTopVisuals(@NonNull final Key key, @NonNull final Canvas canvas, @NonNull final Paint paint, @NonNull final KeyDrawParams params) { @@ -466,7 +527,9 @@ protected void onDrawKeyTopVisuals(@NonNull final Key key, @NonNull final Canvas } if (key.isEnabled()) { - if (StringUtilsKt.isEmoji(label)) + if (mKeyCustomBgColors.containsKey(key)) { + paint.setColor(getContrastingColor(mKeyCustomBgColors.get(key))); + } else if (StringUtilsKt.isEmoji(label)) paint.setColor(key.selectTextColor(params) | 0xFF000000); // ignore alpha for emojis (though // actually color isn't applied anyway and // we could just set white) @@ -476,7 +539,8 @@ else if (this instanceof EmojiPageKeyboardView) paint.setColor(mColors.get(ColorType.EMOJI_KEY_TEXT)); else if (this instanceof PopupKeysKeyboardView) { if (key.isPressed()) { - paint.setColor(Color.BLACK); // Focused key: Black text + int pressedBgColor = mColors.getPressedColor(key.hasActionKeyBackground() ? ColorType.ACTION_KEY_POPUP_KEYS_BACKGROUND : ColorType.POPUP_KEYS_BACKGROUND); + paint.setColor(getContrastingColor(pressedBgColor)); } else { paint.setColor(mColors.get(ColorType.POPUP_KEY_TEXT)); // Unfocused: Theme default } @@ -638,7 +702,7 @@ public Paint newLabelPaint(@Nullable final Key key) { * because the keyboard renders the keys to an off-screen buffer and an * invalidate() only * draws the cached buffer. - * + * * @see #invalidateKey(Key) */ public void invalidateAllKeys() { @@ -653,7 +717,7 @@ public void invalidateAllKeys() { * one key is changing it's content. Any changes that affect the position or * size of the key * may not be honored. - * + * * @param key key in the attached {@link Keyboard}. * @see #invalidateAllKeys */ @@ -677,8 +741,42 @@ public void deallocateMemory() { freeOffscreenBuffer(); } + private static int getCompositeColor(int srcColor, int dstColor) { + int alpha = (srcColor >> 24) & 0xFF; + if (alpha == 0xFF) return srcColor; + if (alpha == 0x00) return dstColor; + + float fAlpha = alpha / 255f; + int srcR = (srcColor >> 16) & 0xFF; + int srcG = (srcColor >> 8) & 0xFF; + int srcB = srcColor & 0xFF; + + int dstR = (dstColor >> 16) & 0xFF; + int dstG = (dstColor >> 8) & 0xFF; + int dstB = dstColor & 0xFF; + + int r = Math.round(srcR * fAlpha + dstR * (1 - fAlpha)); + int g = Math.round(srcG * fAlpha + dstG * (1 - fAlpha)); + int b = Math.round(srcB * fAlpha + dstB * (1 - fAlpha)); + + return 0xFF000000 | (r << 16) | (g << 8) | b; + } + + private int getContrastingColor(int bgColor) { + int baseBg = mColors.get(ColorType.MAIN_BACKGROUND); + int compositeBg = getCompositeColor(bgColor, baseBg); + double Lbg = androidx.core.graphics.ColorUtils.calculateLuminance(compositeBg); + double Lwhite = 0.95; + double Lblack = 0.015; + double ratioWhite = Lbg > Lwhite ? (Lbg + 0.05) / (Lwhite + 0.05) : (Lwhite + 0.05) / (Lbg + 0.05); + double ratioBlack = Lbg > Lblack ? (Lbg + 0.05) / (Lblack + 0.05) : (Lblack + 0.05) / (Lbg + 0.05); + return ratioWhite > ratioBlack ? 0xFFFAFAFA : 0xFF222222; + } + private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) { - if (key.hasActionKeyBackground()) { + if (mKeyCustomBgColors.containsKey(key)) { + icon.setColorFilter(getContrastingColor(mKeyCustomBgColors.get(key)), android.graphics.PorterDuff.Mode.SRC_IN); + } else if (key.hasActionKeyBackground()) { mColors.setColor(icon, ColorType.ACTION_KEY_ICON); } else if (key.isShift() && keyboard != null) { if (keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED @@ -692,7 +790,8 @@ private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) { mColors.setColor(icon, ColorType.KEY_ICON); } else if (this instanceof PopupKeysKeyboardView) { if (key.isPressed()) { - icon.setColorFilter(Color.BLACK, android.graphics.PorterDuff.Mode.SRC_IN); + int pressedBgColor = mColors.getPressedColor(key.hasActionKeyBackground() ? ColorType.ACTION_KEY_POPUP_KEYS_BACKGROUND : ColorType.POPUP_KEYS_BACKGROUND); + icon.setColorFilter(getContrastingColor(pressedBgColor), android.graphics.PorterDuff.Mode.SRC_IN); } else { mColors.setColor(icon, ColorType.POPUP_KEY_ICON); } @@ -704,5 +803,4 @@ private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) { mColors.setColor(icon, ColorType.KEY_TEXT); } } - } diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index 96399c446..142bbaf9f 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -132,7 +132,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy private final KeyPreviewChoreographer mKeyPreviewChoreographer; // More keys keyboard - private final Paint mBackgroundDimAlphaPaint = new Paint(); // todo: not used at all + private final View mPopupKeysKeyboardContainer; private final View mPopupKeysKeyboardForActionContainer; private final WeakHashMap mPopupKeysKeyboardCache = new WeakHashMap<>(); @@ -186,10 +186,7 @@ public MainKeyboardView(final Context context, final AttributeSet attrs, final i && !forceNonDistinctMultitouch; mNonDistinctMultitouchHelper = hasDistinctMultitouch ? null : new NonDistinctMultitouchHelper(); - final int backgroundDimAlpha = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_backgroundDimAlpha, 0); - mBackgroundDimAlphaPaint.setColor(Color.BLACK); - mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha); + mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction( R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f) * Settings.getValues().mFontSizeMultiplier; @@ -802,12 +799,18 @@ public boolean processMotionEvent(final MotionEvent event) { return true; } + public void dismissAllKeyPreviews() { + mKeyPreviewChoreographer.clear(); + mDrawingPreviewPlacerView.removeAllViews(); + } + public void cancelAllOngoingEvents() { mTimerHandler.cancelAllMessages(); PointerTracker.setReleasedKeyGraphicsToAllKeys(); mGestureFloatingTextDrawingPreview.dismissGestureFloatingPreviewText(); mSlidingKeyInputDrawingPreview.dismissSlidingKeyInputPreview(); PointerTracker.dismissAllPopupKeysPanels(); + dismissAllKeyPreviews(); PointerTracker.cancelAllPointerTrackers(); } diff --git a/app/src/main/java/helium314/keyboard/keyboard/TextEditView.java b/app/src/main/java/helium314/keyboard/keyboard/TextEditView.java deleted file mode 100644 index 7005f41a4..000000000 --- a/app/src/main/java/helium314/keyboard/keyboard/TextEditView.java +++ /dev/null @@ -1,231 +0,0 @@ -// 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/emoji/EmojiPalettesView.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java index b7ed505a8..e7dd8db6b 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java @@ -1169,6 +1169,10 @@ private void downloadEmojiDictionary() { try { java.net.URL url = new java.net.URL(urlStr); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "HeliboardL/3.8.9 (Android)"); + conn.setConnectTimeout(15000); + conn.setReadTimeout(15000); + conn.setInstanceFollowRedirects(true); conn.connect(); if (conn.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) { @@ -1188,7 +1192,7 @@ private void downloadEmojiDictionary() { } // Success! Switch back to UI thread - new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + EmojiPalettesView.this.post(() -> { Toast.makeText(getContext(), "Emoji dictionary installed!", Toast.LENGTH_SHORT).show(); initDictionaryFacilitator(); mIsDownloadingEmojiDict = false; @@ -1202,7 +1206,7 @@ private void downloadEmojiDictionary() { } } catch (Exception e) { android.util.Log.e("EmojiSearch", "Failed to download dictionary", e); - new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + EmojiPalettesView.this.post(() -> { Toast.makeText(getContext(), "Failed to download dictionary", Toast.LENGTH_SHORT).show(); mIsDownloadingEmojiDict = false; if (mInSearchMode) { diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyPreviewChoreographer.java b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyPreviewChoreographer.java index 91ea032eb..6a20c7574 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyPreviewChoreographer.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyPreviewChoreographer.java @@ -144,4 +144,12 @@ void showKeyPreview(final Key key, final KeyPreviewView keyPreviewView) { mShowingKeyPreviewViews.put(key, keyPreviewView); } + public void clear() { + for (KeyPreviewView view : mShowingKeyPreviewViews.values()) { + view.setVisibility(View.INVISIBLE); + } + mShowingKeyPreviewViews.clear(); + mFreeKeyPreviewViews.clear(); + } + } 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 ec6027d33..b66fa071d 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt @@ -154,6 +154,7 @@ class KeyboardIconsSet private constructor() { ToolbarKey.SETTINGS -> R.drawable.sym_keyboard_settings_holo ToolbarKey.SELECT_ALL -> R.drawable.ic_select_all ToolbarKey.SELECT_WORD -> R.drawable.ic_select + ToolbarKey.SELECT_MODE -> R.drawable.ic_select ToolbarKey.COPY -> R.drawable.sym_keyboard_copy ToolbarKey.CUT -> R.drawable.sym_keyboard_cut ToolbarKey.PASTE -> R.drawable.sym_keyboard_paste @@ -238,6 +239,7 @@ class KeyboardIconsSet private constructor() { ToolbarKey.SETTINGS -> R.drawable.sym_keyboard_settings_lxx ToolbarKey.SELECT_ALL -> R.drawable.ic_select_all ToolbarKey.SELECT_WORD -> R.drawable.ic_select + ToolbarKey.SELECT_MODE -> R.drawable.ic_select ToolbarKey.COPY -> R.drawable.sym_keyboard_copy ToolbarKey.CUT -> R.drawable.sym_keyboard_cut ToolbarKey.PASTE -> R.drawable.sym_keyboard_paste @@ -322,6 +324,7 @@ class KeyboardIconsSet private constructor() { ToolbarKey.SETTINGS -> R.drawable.sym_keyboard_settings_rounded ToolbarKey.SELECT_ALL -> R.drawable.ic_select_all_rounded ToolbarKey.SELECT_WORD -> R.drawable.ic_select_rounded + ToolbarKey.SELECT_MODE -> R.drawable.ic_select_rounded ToolbarKey.COPY -> R.drawable.sym_keyboard_copy_rounded ToolbarKey.CUT -> R.drawable.sym_keyboard_cut_rounded ToolbarKey.PASTE -> R.drawable.sym_keyboard_paste_rounded diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt index 40d4a91c1..ac69e9bc5 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt @@ -58,6 +58,7 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co KeyboardId.ELEMENT_EMOJI_BOTTOM_ROW -> LayoutType.EMOJI_BOTTOM KeyboardId.ELEMENT_CLIPBOARD_BOTTOM_ROW -> LayoutType.CLIPBOARD_BOTTOM KeyboardId.ELEMENT_HANDWRITING_BOTTOM_ROW -> LayoutType.HANDWRITING_BOTTOM + KeyboardId.ELEMENT_TEXT_EDIT -> LayoutType.EDITING else -> LayoutType.MAIN } val baseKeys = LayoutParser.parseLayout(layoutType, params, context) diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LocaleKeyboardInfos.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LocaleKeyboardInfos.kt index 79a2d6465..0fdb367f1 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LocaleKeyboardInfos.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LocaleKeyboardInfos.kt @@ -11,7 +11,6 @@ import helium314.keyboard.latin.R import helium314.keyboard.latin.common.splitOnFirstSpacesOnly import helium314.keyboard.latin.common.splitOnWhitespace import helium314.keyboard.latin.settings.Settings -import helium314.keyboard.latin.utils.SpacedTokens import helium314.keyboard.latin.utils.SubtypeLocaleUtils import java.io.InputStream import java.util.Locale @@ -84,7 +83,7 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) { READER_MODE_EXTRA_KEYS -> if (!onlyPopupKeys) addExtraKey(line.split(colonSpaceRegex, 2)) READER_MODE_LABELS -> if (!onlyPopupKeys) addLabel(line.split(colonSpaceRegex, 2)) READER_MODE_NUMBER_ROW -> localizedNumberKeys = line.splitOnWhitespace() - READER_MODE_TLD -> tlds.addAll(SpacedTokens(line).map { ".$it" }) + READER_MODE_TLD -> tlds.addAll(line.splitOnWhitespace().map { ".$it" }) } } } @@ -160,11 +159,11 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) { tlds.add(0, comTld) val ccLower = locale.country.lowercase() if (ccLower.isNotEmpty() && locale.language != SubtypeLocaleUtils.NO_LANGUAGE) { - specialCountryTlds[ccLower]?.let { tlds.addAll(SpacedTokens(it)) } ?: tlds.add(".$ccLower") + specialCountryTlds[ccLower]?.let { tlds.addAll(it.splitOnWhitespace()) } ?: tlds.add(".$ccLower") } if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country)) tlds.add(".eu") - tlds.addAll(SpacedTokens(otherDefaultTlds)) + tlds.addAll(otherDefaultTlds.splitOnWhitespace()) } } 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 8c1ef3cfb..2f25f25d0 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 @@ -102,6 +102,7 @@ object KeyCode { const val SETTINGS = -301 const val TOGGLE_TEXT_EDIT_MODE = -305 + const val TOGGLE_SELECTION_MODE = -306 const val CURRENCY_SLOT_1 = -801 const val CURRENCY_SLOT_2 = -802 @@ -228,7 +229,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, TOGGLE_TEXT_EDIT_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, TOGGLE_SELECTION_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 ae15f16f8..50b67c15d 100644 --- a/app/src/main/java/helium314/keyboard/latin/ClipboardHistoryManager.kt +++ b/app/src/main/java/helium314/keyboard/latin/ClipboardHistoryManager.kt @@ -42,6 +42,10 @@ class ClipboardHistoryManager( private var clipboardDao: ClipboardDao? = null private var dontShowCurrentSuggestion: Boolean = false private var mediaStoreObserver: ContentObserver? = null + // ponytail: track last clip state to avoid resetting dismiss state on duplicate events + private var lastPrimaryClipText: String? = null + private var lastPrimaryClipUri: String? = null + private var lastPrimaryClipTimestamp: Long = 0L private data class ScreenshotInfo( val uri: Uri, @@ -120,7 +124,7 @@ class ClipboardHistoryManager( ) cachedScreenshotInfo = ScreenshotInfo(contentUri, fileName, fullPath, dateAdded) if (onComplete != null) { - Handler(Looper.getMainLooper()).post { onComplete() } + mainHandler.post { onComplete() } } return@thread } @@ -134,7 +138,7 @@ class ClipboardHistoryManager( } cachedScreenshotInfo = null if (onComplete != null) { - Handler(Looper.getMainLooper()).post { onComplete() } + mainHandler.post { onComplete() } } } } @@ -143,6 +147,17 @@ class ClipboardHistoryManager( clipboardManager = latinIME.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.addPrimaryClipChangedListener(this) clipboardDao = ClipboardDao.getInstance(latinIME) + // ponytail: initialize last clip state + try { + val clipData = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + lastPrimaryClipText = clipData.getItemAt(0)?.coerceToText(latinIME)?.toString() + lastPrimaryClipUri = clipData.getItemAt(0)?.uri?.toString() + lastPrimaryClipTimestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) clipData.description.timestamp else 0L + } + } catch (e: Exception) { + // Ignore + } if (latinIME.mSettings.current.mClipboardHistoryEnabled) thread { fetchPrimaryClip() } thread { cleanUpImageCache() } @@ -205,10 +220,42 @@ class ClipboardHistoryManager( override fun onPrimaryClipChanged() { // Make sure we read clipboard content only if history settings is set if (latinIME.mSettings.current.mClipboardHistoryEnabled) { - thread { fetchPrimaryClip() } - dontShowCurrentSuggestion = false - val prefs = latinIME.prefs() - prefs.edit().remove("last_dismissed_clipboard_text").apply() + // ponytail: ignore duplicate events where clipboard contents didn't actually change + val clipData = try { + clipboardManager.primaryClip + } catch (e: Exception) { + null + } + val currentText = if (clipData != null && clipData.itemCount > 0) { + clipData.getItemAt(0)?.coerceToText(latinIME)?.toString() + } else { + null + } + val currentUri = if (clipData != null && clipData.itemCount > 0) { + clipData.getItemAt(0)?.uri?.toString() + } else { + null + } + val currentTimestamp = if (clipData != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + clipData.description.timestamp + } else { + 0L + } + + val hasChanged = currentText != lastPrimaryClipText + || currentUri != lastPrimaryClipUri + || (currentTimestamp != 0L && currentTimestamp != lastPrimaryClipTimestamp) + + if (hasChanged) { + lastPrimaryClipText = currentText + lastPrimaryClipUri = currentUri + lastPrimaryClipTimestamp = currentTimestamp + + thread { fetchPrimaryClip() } + dontShowCurrentSuggestion = false + val prefs = latinIME.prefs() + prefs.edit().remove("last_dismissed_clipboard_text").apply() + } } } @@ -492,7 +539,7 @@ class ClipboardHistoryManager( thread { val cachedPath = cacheImage(contentUri) if (cachedPath != null) { - Handler(Looper.getMainLooper()).post { + mainHandler.post { clipboardDao?.addClip(System.currentTimeMillis(), false, "[Screenshot]", cachedPath) } } diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitator.java b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitator.java index 559865ad8..5f91d6aaf 100644 --- a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitator.java +++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitator.java @@ -19,8 +19,10 @@ import helium314.keyboard.latin.settings.SettingsValuesForSuggestion; import helium314.keyboard.latin.utils.SuggestionResults; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -159,4 +161,14 @@ void unlearnFromUserHistory(final String word, void dumpDictionaryForDebug(final String dictName); @NonNull List getDictionaryStats(final Context context); + + /** + * Returns all words with frequencies from the primary main dictionary, for gesture typing + * precomputation. Iterates the binary dictionary directly; can be slow on first call. + * The default returns an empty map; DictionaryFacilitatorImpl overrides this. + */ + @NonNull + default Map getAllMainDictionaryWordsWithFrequency() { + return Collections.emptyMap(); + } } diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt index 28bbc9c8d..30a08d30a 100644 --- a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt +++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt @@ -383,14 +383,8 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator { ) ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(WordInfo(currentWord)) - // Un-blacklist a word the user deliberately committed — but ONLY if it is a word they - // genuinely know: present in a non-history dictionary (main/contacts/apps or their personal - // dictionary). A junk word (e.g. a gesture misfire that is in no dictionary) must STAY - // blacklisted, otherwise the user's "remove" never sticks: it would be un-blacklisted here - // and then re-learned, resurrecting it (e.g. "לא" → "לר"). - dictionaryGroups.filter { it.confidence == preferredGroup.confidence }.forEach { - if (it.isInNonHistoryDictionary(currentWord)) it.removeFromBlacklist(currentWord) - } + // Upstream v3.9.1: do not automatically remove blacklisted words from the blacklist on type. + // Explicit Add/Blocklist actions remain the safe path for restoring blocked words. } } @@ -542,6 +536,24 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator { putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.lowercase(Locale.getDefault())) } + override fun getAllMainDictionaryWordsWithFrequency(): Map { + val result = mutableMapOf() + val dictGroup = dictionaryGroups.firstOrNull() ?: return emptyMap() + val mainDict = dictGroup.getDict(Dictionary.TYPE_MAIN) + if (mainDict != null) { + result.putAll(mainDict.getAllWordsWithFrequency()) + } + val userHistoryDict = dictGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) + if (userHistoryDict != null) { + result.putAll(userHistoryDict.getAllWordsWithFrequency()) + } + val userDict = dictGroup.getSubDict(Dictionary.TYPE_USER) + if (userDict != null) { + result.putAll(userDict.getAllWordsWithFrequency()) + } + return result + } + // TODO: Revise the way to fusion suggestion results. override fun getSuggestionResults( composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard, diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java index cf6ede855..a1874689c 100644 --- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java +++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java @@ -865,6 +865,15 @@ public void onFinishInput() { mFloatingKeyboardManager.hide(false); } } + // ponytail: reset text edit mode when input finishes if persist is false + if (KeyboardActionListenerImpl.sPersistentTextEditModeActive) { + if (!Settings.getInstance().getCurrent().mPersistTextEditMode) { + KeyboardActionListenerImpl.sPersistentTextEditModeActive = false; + if (mKeyboardSwitcher != null) { + mKeyboardSwitcher.hideTextEditView(); + } + } + } } @Override @@ -1732,6 +1741,13 @@ public void pickSuggestionManually(final SuggestedWordInfo suggestionInfo) { emojiView.addRecentKey(suggestionInfo.mWord); } } + + // ponytail: self-learning — bump gesture rank for words the user picks from fallback engine + if (suggestionInfo.isKindOf(helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo.KIND_CORRECTION) + && helium314.keyboard.latin.dictionary.Dictionary.DICTIONARY_USER_TYPED.equals( + suggestionInfo.mSourceDict != null ? suggestionInfo.mSourceDict.mDictType : "")) { + helium314.keyboard.latin.gesture.SwipeGestureEngine.recordAccepted(suggestionInfo.mWord); + } } /** diff --git a/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt b/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt index cf7176dc7..4c9fe480c 100644 --- a/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt +++ b/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt @@ -49,7 +49,7 @@ class SingleDictionaryFacilitator(private val dict: Dictionary) : DictionaryFaci ComposedData.createForWord(word), NgramContext.getEmptyPrevWordsContext(0), KeyboardSwitcher.getInstance().keyboard, // looks like actual keyboard doesn't matter (composed data doesn't contain coordinates) - SettingsValuesForSuggestion(false, false), + SettingsValuesForSuggestion(false, false, "fallback"), Suggest.SESSION_ID_TYPING, SuggestedWords.INPUT_STYLE_TYPING ) return suggestionResults diff --git a/app/src/main/java/helium314/keyboard/latin/Suggest.kt b/app/src/main/java/helium314/keyboard/latin/Suggest.kt index 1cd59b4ae..952d22fc6 100644 --- a/app/src/main/java/helium314/keyboard/latin/Suggest.kt +++ b/app/src/main/java/helium314/keyboard/latin/Suggest.kt @@ -18,11 +18,13 @@ import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.define.DecoderSpecificConstants.SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION import helium314.keyboard.latin.define.DecoderSpecificConstants.SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION import helium314.keyboard.latin.dictionary.Dictionary +import helium314.keyboard.latin.gesture.SwipeGestureEngine import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.SettingsValuesForSuggestion import helium314.keyboard.latin.suggestions.SuggestionStripView import helium314.keyboard.latin.utils.AutoCorrectionUtils import helium314.keyboard.latin.utils.Log +import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.SuggestionResults import java.util.Locale import kotlin.math.min @@ -41,6 +43,12 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { // Optionally log evicted entries for debugging } } + // Precomputed gesture word index, keyed by a fingerprint of the key positions. + // Rebuilt only when key centres actually change (language/layout switch), not on shift-state + // or action-button changes, and not on text-field focus changes. + @Volatile private var gestureIndex: SwipeGestureEngine.GestureIndex? = null + @Volatile private var gestureIndexFingerprint: Int = 0 + // Cached scoreLimit to avoid repeated Settings lookups in hot path // The read-then-write of (mLastScoreLimitUpdateTime, mCachedScoreLimitForAutocorrect) // is guarded by `synchronized(this)` in shouldBeAutoCorrected() to make the update atomic @@ -250,11 +258,15 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { // Score is too low for autocorrect — but for long words, the normalized score // formula penalizes proportionally (weight = 1 - editDist/len), so a single typo // in a 12-char word gets unfairly suppressed. Use a relaxed threshold for long words. - if (consideredWord.length > 6 && firstSuggestion.mScore > scoreLimit / 2) { + // ponytail: relax limits for misspelled words (not in dictionary) to match Gboard-like behaviour + val isTypedWordInDict = typedWordInfo != null + val minScore = if (isTypedWordInDict) (scoreLimit / 2) else (scoreLimit / 4) + val minLength = if (isTypedWordInDict) 6 else 3 + if (consideredWord.length > minLength && firstSuggestion.mScore > minScore) { val normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( consideredWord, firstSuggestion.mWord, firstSuggestion.mScore) val adjustedThreshold = mAutoCorrectionThreshold * - (6f / consideredWord.length.coerceAtMost(15)) + (minLength.toFloat() / consideredWord.length.coerceAtMost(15)) if (normalizedScore < adjustedThreshold) { return true to false } @@ -326,10 +338,34 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { settingsValuesForSuggestion: SettingsValuesForSuggestion, inputStyle: Int, sequenceNumber: Int ): SuggestedWords { - val suggestionResults = mDictionaryFacilitator.getSuggestionResults( - wordComposer.composedDataSnapshot, ngramContext, keyboard, - settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle - ) + val pointers = wordComposer.composedDataSnapshot.mInputPointers + val useFallback = "fallback" == settingsValuesForSuggestion.mGestureMethod || !JniUtils.sHaveNativeGestureLib + val suggestionResults = if (useFallback) { + val fingerprint = SwipeGestureEngine.layoutFingerprint(keyboard) + var index = gestureIndex + if (index == null || index.byFirst.isEmpty() || gestureIndexFingerprint != fingerprint) { + val words = mDictionaryFacilitator.getAllMainDictionaryWordsWithFrequency() + index = SwipeGestureEngine.buildIndex(words, keyboard) + if (index.byFirst.isNotEmpty()) { + gestureIndex = index + gestureIndexFingerprint = fingerprint + } + } + val predictionSet = if (ngramContext.isValid) { + mDictionaryFacilitator.getSuggestionResults( + ComposedData(InputPointers(32), false, ""), ngramContext, keyboard, + settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle + ).map { it.mWord.lowercase(Locale.ROOT) }.toSet() + } else { + emptySet() + } + SwipeGestureEngine.rankByIndex(index, pointers, keyboard, SuggestedWords.MAX_SUGGESTIONS, predictionSet) + } else { + mDictionaryFacilitator.getSuggestionResults( + wordComposer.composedDataSnapshot, ngramContext, keyboard, + settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle + ) + } replaceSingleLetterFirstSuggestion(suggestionResults) // For transforming words that don't come from a dictionary, because it's our best bet @@ -343,8 +379,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { || keyboardShiftMode == WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED if (shouldMakeSuggestionsOnlyFirstCharCapitalized || shouldMakeSuggestionsAllUpperCase) { for (i in 0 until suggestionsCount) { - val wordInfo = suggestionsContainer[i] - val wordLocale = wordInfo!!.mSourceDict.mLocale + val wordInfo = suggestionsContainer[i] ?: continue + val wordLocale = wordInfo.mSourceDict.mLocale val transformedWordInfo = getTransformedSuggestedWordInfo( wordInfo, wordLocale ?: locale, shouldMakeSuggestionsAllUpperCase, shouldMakeSuggestionsOnlyFirstCharCapitalized, 0 @@ -353,8 +389,9 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { } } val rejected: SuggestedWordInfo? - if (SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION && suggestionsContainer.size > 1 && TextUtils.equals( - suggestionsContainer[0]!!.mWord, + val firstSuggestion = suggestionsContainer.firstOrNull() + if (SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION && suggestionsContainer.size > 1 && firstSuggestion != null && TextUtils.equals( + firstSuggestion.mWord, wordComposer.rejectedBatchModeSuggestion ) ) { diff --git a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt index cf49ca27e..1c4d47486 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt @@ -49,6 +49,9 @@ interface Colors { /** get the colorInt */ @ColorInt fun get(color: ColorType): Int + /** get the pressed state colorInt */ + @ColorInt fun getPressedColor(color: ColorType): Int + /** apply a color to the [drawable], may be through color filter or tint (with or without state list) */ fun setColor(drawable: Drawable, color: ColorType) @@ -293,6 +296,11 @@ class DynamicColors(context: Context, override val themeStyle: String, override NAVIGATION_BAR -> navBar MORE_SUGGESTIONS_HINT, SUGGESTED_WORD, SUGGESTION_TYPED_WORD, SUGGESTION_VALID_WORD -> adjustedKeyText ACTION_KEY_ICON, TOOL_BAR_EXPAND_KEY -> Color.WHITE + EDIT_MODE_DELETE_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(accent, functionalKey, 0.4f) + EDIT_MODE_FUNC_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(accent, functionalKey, 0.2f) + EDIT_MODE_ALPHA_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(accent, functionalKey, 0.6f) + EDIT_MODE_NAV_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(keyBackground, functionalKey, 0.3f) + EDIT_MODE_JUMP_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(keyBackground, functionalKey, 0.7f) } override fun setColor(drawable: Drawable, color: ColorType) { @@ -319,6 +327,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override override fun setColor(view: ImageView, color: ColorType) { if (color == TOOL_BAR_KEY) { + view.clearColorFilter() setColor(view.drawable, color) return } @@ -357,6 +366,20 @@ class DynamicColors(context: Context, override val themeStyle: String, override } else -> view.background.colorFilter = backgroundFilter } + } + + override fun getPressedColor(color: ColorType): Int { + if (color == ColorType.ACTION_KEY_POPUP_KEYS_BACKGROUND) { + return if (themeStyle == STYLE_HOLO) adjustedBackground else accent + } + return if (themeStyle == STYLE_HOLO) { + accent + } else if (isNight) { + if (hasKeyBorders) doubleAdjustedAccent + else adjustedAccent + } else { + accent + } } } @@ -488,6 +511,11 @@ class DefaultColors ( SUGGESTION_AUTO_CORRECT, EMOJI_CATEGORY, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY, ONE_HANDED_MODE_BUTTON -> suggestionText MORE_SUGGESTIONS_HINT, SUGGESTED_WORD, SUGGESTION_TYPED_WORD, SUGGESTION_VALID_WORD -> adjustedSuggestionText ACTION_KEY_ICON -> Color.WHITE + EDIT_MODE_DELETE_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(accent, functionalKey, 0.4f) + EDIT_MODE_FUNC_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(accent, functionalKey, 0.2f) + EDIT_MODE_ALPHA_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(accent, functionalKey, 0.6f) + EDIT_MODE_NAV_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(keyBackground, functionalKey, 0.3f) + EDIT_MODE_JUMP_BACKGROUND -> androidx.core.graphics.ColorUtils.blendARGB(keyBackground, functionalKey, 0.7f) } override fun setColor(drawable: Drawable, color: ColorType) { @@ -514,6 +542,7 @@ class DefaultColors ( override fun setColor(view: ImageView, color: ColorType) { if (color == TOOL_BAR_KEY) { + view.clearColorFilter() setColor(view.drawable, color) return } @@ -549,6 +578,13 @@ class DefaultColors ( ACTION_KEY_ICON -> actionKeyIconColorFilter else -> colorFilter(get(color)) // create color filter (not great for performance, so the frequently used filters should be stored) } + + override fun getPressedColor(color: ColorType): Int { + if (color == ColorType.POPUP_KEYS_BACKGROUND || color == ColorType.ACTION_KEY_POPUP_KEYS_BACKGROUND) { + return doubleAdjustedBackground + } + return brightenOrDarken(get(color), true) + } } class AllColors(private val colorMap: EnumMap, override val themeStyle: String, override val hasKeyBorders: Boolean, backgroundImage: Drawable?) : Colors { @@ -566,6 +602,7 @@ class AllColors(private val colorMap: EnumMap, override val them override fun setColor(view: ImageView, color: ColorType) { if (color == TOOL_BAR_KEY) { + view.clearColorFilter() setColor(view.drawable, color) return } @@ -591,6 +628,10 @@ class AllColors(private val colorMap: EnumMap, override val them } private fun getColorFilter(color: ColorType) = colorFilters.getOrPut(color) { colorFilter(get(color)) } + + override fun getPressedColor(color: ColorType): Int { + return brightenOrDarken(get(color), true) + } } private fun colorFilter(color: Int, mode: BlendModeCompat = BlendModeCompat.MODULATE): ColorFilter { @@ -650,6 +691,11 @@ enum class ColorType { TOOL_BAR_EXPAND_KEY_BACKGROUND, TOOL_BAR_KEY, TOOL_BAR_KEY_ENABLED_BACKGROUND, + EDIT_MODE_DELETE_BACKGROUND, + EDIT_MODE_FUNC_BACKGROUND, + EDIT_MODE_ALPHA_BACKGROUND, + EDIT_MODE_NAV_BACKGROUND, + EDIT_MODE_JUMP_BACKGROUND, MAIN_BACKGROUND, } diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt index 0e1b4d48e..0596e5fd4 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt @@ -7,7 +7,6 @@ import helium314.keyboard.latin.common.StringUtils.mightBeEmoji import helium314.keyboard.latin.common.StringUtils.newSingleCodePointString import helium314.keyboard.latin.settings.SpacingAndPunctuations import helium314.keyboard.latin.utils.ScriptUtils -import helium314.keyboard.latin.utils.SpacedTokens import helium314.keyboard.latin.utils.SpannableStringUtils import helium314.keyboard.latin.utils.TextRange import java.math.BigInteger @@ -272,7 +271,7 @@ fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointStri /** returns whether the text is a single emoji */ fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex) -fun String.splitOnWhitespace() = SpacedTokens(this).toList() +fun String.splitOnWhitespace() = split(Regex("\\s+")).filter { it.isNotEmpty() } // from https://github.com/mathiasbynens/emoji-test-regex-pattern, MIT license // matches single emojis only diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/AppsBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/dictionary/AppsBinaryDictionary.java index 864d99856..e88987a35 100644 --- a/app/src/main/java/helium314/keyboard/latin/dictionary/AppsBinaryDictionary.java +++ b/app/src/main/java/helium314/keyboard/latin/dictionary/AppsBinaryDictionary.java @@ -10,7 +10,6 @@ import helium314.keyboard.latin.NgramContext; import helium314.keyboard.latin.common.StringUtils; import helium314.keyboard.latin.utils.Log; -import helium314.keyboard.latin.utils.SpacedTokens; // todo: actually we only need a single AppsBinaryDictionary, but currently // have one for each language, and may even have multiple instances in multilingual typing @@ -75,7 +74,8 @@ private void addNameLocked(final String appLabel) { NgramContext ngramContext = NgramContext.getEmptyPrevWordsContext( BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM); // TODO: Better tokenization for non-Latin writing systems - for (final String word : new SpacedTokens(appLabel)) { + for (final String word : appLabel.split("\\s+")) { + if (word.isEmpty()) continue; if (DEBUG_DUMP) { Log.d(TAG, "addName word = " + word); } diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/Dictionary.java b/app/src/main/java/helium314/keyboard/latin/dictionary/Dictionary.java index 9a5cb4398..734d3d706 100644 --- a/app/src/main/java/helium314/keyboard/latin/dictionary/Dictionary.java +++ b/app/src/main/java/helium314/keyboard/latin/dictionary/Dictionary.java @@ -7,7 +7,9 @@ package helium314.keyboard.latin.dictionary; import java.util.ArrayList; +import java.util.Collections; import java.util.Locale; +import java.util.Map; import helium314.keyboard.latin.NgramContext; import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo; @@ -98,6 +100,16 @@ public boolean isValidWord(final String word) { */ abstract public boolean isInDictionary(final String word); + /** + * Returns all words stored in this dictionary. + * The default implementation returns an empty list; override in concrete dictionaries + * that support full enumeration (e.g. ReadOnlyBinaryDictionary). + */ + @androidx.annotation.NonNull + public Map getAllWordsWithFrequency() { + return Collections.emptyMap(); + } + /** * Get the frequency of the word. * @param word the word to get the frequency of. diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryCollection.java b/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryCollection.java index e746446da..5e5b4de22 100644 --- a/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryCollection.java +++ b/app/src/main/java/helium314/keyboard/latin/dictionary/DictionaryCollection.java @@ -16,7 +16,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; /** * Class for a collection of dictionaries that behave like one dictionary. @@ -89,6 +91,14 @@ public int getMaxFrequencyOfExactMatches(final String word) { return maxFreq; } + @Override + @androidx.annotation.NonNull + public Map getAllWordsWithFrequency() { + Map result = new HashMap<>(); + for (Dictionary dict : mDictionaries) result.putAll(dict.getAllWordsWithFrequency()); + return result; + } + @Override public boolean isInitialized() { return !mDictionaries.isEmpty(); diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/ExpandableBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/dictionary/ExpandableBinaryDictionary.java index 40078556c..2363a9b8a 100644 --- a/app/src/main/java/helium314/keyboard/latin/dictionary/ExpandableBinaryDictionary.java +++ b/app/src/main/java/helium314/keyboard/latin/dictionary/ExpandableBinaryDictionary.java @@ -425,6 +425,40 @@ protected boolean isInDictionaryLocked(final String word) { return mBinaryDictionary.isInDictionary(word); } + @Override + public Map getAllWordsWithFrequency() { + Map words = new java.util.HashMap<>(); + boolean lockAcquired = false; + try { + lockAcquired = mLock.readLock().tryLock( + TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + if (lockAcquired) { + if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) { + return words; + } + int token = 0; + do { + BinaryDictionary.GetNextWordAndFrequencyResult result = + mBinaryDictionary.getNextWordAndFrequency(token); + if (result.mWordAndFrequency == null) break; + String word = result.mWordAndFrequency.mWord; + int freq = result.mWordAndFrequency.mFrequency; + if (word != null && !word.isEmpty() && freq >= 0) { + words.put(word, freq); + } + token = result.mNextToken; + } while (token != 0); + } + } catch (final InterruptedException e) { + Log.e(TAG, "Interrupted tryLock() in getAllWordsWithFrequency().", e); + } finally { + if (lockAcquired) { + mLock.readLock().unlock(); + } + } + return words; + } + @Override public int getMaxFrequencyOfExactMatches(final String word) { reloadDictionaryIfRequired(); diff --git a/app/src/main/java/helium314/keyboard/latin/dictionary/ReadOnlyBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/dictionary/ReadOnlyBinaryDictionary.java index 170f57042..63764d7be 100644 --- a/app/src/main/java/helium314/keyboard/latin/dictionary/ReadOnlyBinaryDictionary.java +++ b/app/src/main/java/helium314/keyboard/latin/dictionary/ReadOnlyBinaryDictionary.java @@ -15,7 +15,9 @@ import helium314.keyboard.latin.settings.SettingsValuesForSuggestion; import java.util.ArrayList; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; /** @@ -109,6 +111,55 @@ public int getMaxFrequencyOfExactMatches(final String word) { return NOT_A_PROBABILITY; } + @Override + @androidx.annotation.NonNull + public Map getAllWordsWithFrequency() { + Map words = new HashMap<>(); + int token = 0; + int count = 0; + do { + if (!mLock.readLock().tryLock()) { + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + continue; + } + try { + if (!mBinaryDictionary.isValidDictionary()) { + break; + } + BinaryDictionary.GetNextWordAndFrequencyResult result = + mBinaryDictionary.getNextWordAndFrequency(token); + if (result.mWordAndFrequency == null) break; + String word = result.mWordAndFrequency.mWord; + int freq = result.mWordAndFrequency.mFrequency; + if (word != null && !word.isEmpty() && freq >= 0) { + words.put(word, freq); + } + token = result.mNextToken; + } finally { + mLock.readLock().unlock(); + } + + count++; + if (count % 200 == 0) { + Thread.yield(); + } + if (count % 2000 == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } while (token != 0); + return words; + } + @Override public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) { if (mLock.readLock().tryLock()) { diff --git a/app/src/main/java/helium314/keyboard/latin/gesture/SwipeGestureEngine.java b/app/src/main/java/helium314/keyboard/latin/gesture/SwipeGestureEngine.java new file mode 100644 index 000000000..e0f6baed6 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/latin/gesture/SwipeGestureEngine.java @@ -0,0 +1,350 @@ +/* + * SwipeGestureEngine - gesture path matching for HeliBoard. + * + * Algorithm: arc-length resampling + L2 distance scoring. + * Each word in the dictionary is pre-mapped to a path of N_PTS evenly-spaced + * (x, y) points (normalized to keyboard dimensions). On gesture end the input + * stroke is resampled the same way and candidates are ranked by L2 distance + * with a small log-frequency bonus. + */ +package helium314.keyboard.latin.gesture; + +import helium314.keyboard.keyboard.Key; +import helium314.keyboard.keyboard.Keyboard; +import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo; +import helium314.keyboard.latin.common.InputPointers; +import helium314.keyboard.latin.dictionary.Dictionary; +import helium314.keyboard.latin.utils.SuggestionResults; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SwipeGestureEngine { + + private static final int N_PTS = 16; + private static final float FREQ_WEIGHT = 0.05f; + + // ── Self-learning: boost words user actually confirmed via gesture ───────── + // ponytail: ConcurrentHashMap so corrections from any thread don't corrupt state + private static final ConcurrentHashMap sUserBoost = new ConcurrentHashMap<>(); + private static final int USER_BOOST_MAX = 50; // cap to avoid runaway inflation + + /** Call when user selects a gesture suggestion — bumps its score in future matches. */ + public static void recordAccepted(String word) { + if (word == null || word.isEmpty()) return; + String key = word.toLowerCase(Locale.ROOT); + sUserBoost.merge(key, 1, (a, b) -> Math.min(a + b, USER_BOOST_MAX)); + } + + // ── Precomputed index ───────────────────────────────────────────────────── + + public static class IndexEntry { + public final String word; + public final float[] path; // length N_PTS*2 + public final int frequency; + // ponytail: cache path length to avoid recomputing every ranking call + public final float pathLen; + IndexEntry(String word, float[] path, int frequency) { + this.word = word; + this.path = path; + this.frequency = frequency; + this.pathLen = pathLength(path); + } + } + + public static class GestureIndex { + public final Map> byFirst; + // ponytail: store charToPos in index so rankByIndex doesn't rebuild it every call + public final float[][] charToPos; + GestureIndex(Map> byFirst, float[][] charToPos) { + this.byFirst = byFirst; + this.charToPos = charToPos; + } + } + + public static GestureIndex buildIndex(Map wordsWithFreq, Keyboard keyboard) { + float[][] charToPos = buildCharToPos(keyboard); + Map> byFirst = new HashMap<>(); + for (Map.Entry entry : wordsWithFreq.entrySet()) { + String raw = entry.getKey(); + int freq = entry.getValue() != null ? entry.getValue() : 0; + // ponytail: apply user boost to freq so self-learned words rank higher immediately + String lk = raw.toLowerCase(Locale.ROOT); + Integer boost = sUserBoost.get(lk); + if (boost != null) freq = Math.min(freq + boost * 5, 255); + if (freq < 3) continue; + String word = lk; + if (word.isEmpty()) continue; + char first = word.charAt(0); + if (first < 'a' || first > 'z') continue; + float[] path = wordPath(word, charToPos); + byFirst.computeIfAbsent(first, k -> new ArrayList<>()) + .add(new IndexEntry(raw, path, freq)); + } + return new GestureIndex(byFirst, charToPos); + } + + public static int layoutFingerprint(Keyboard keyboard) { + return Arrays.deepHashCode(buildCharToPos(keyboard)); + } + + // ── Public matching API ─────────────────────────────────────────────────── + + private static boolean isAsciiLetter(int code) { + return (code >= 'a' && code <= 'z') || (code >= 'A' && code <= 'Z'); + } + + // ponytail: use charToPos directly instead of iterating all keys on every gesture + private static List nearestLettersFromMap(float nx, float ny, float[][] charToPos) { + float minDist = Float.MAX_VALUE; + float[] dists = new float[26]; + for (int i = 0; i < 26; i++) { + float cx = charToPos[i][0], cy = charToPos[i][1]; + if (cx == 0f && cy == 0f) { dists[i] = Float.MAX_VALUE; continue; } + float d = (nx - cx) * (nx - cx) + (ny - cy) * (ny - cy); + dists[i] = d; + if (d < minDist) minDist = d; + } + List results = new ArrayList<>(4); + float threshold = minDist + 0.02f; + for (int i = 0; i < 26; i++) { + if (dists[i] <= threshold) results.add((char) ('a' + i)); + } + return results; + } + + // kept public for external callers (e.g. tests) + public static List nearestLetters(int x, int y, Keyboard keyboard) { + float kw = keyboard.mOccupiedWidth, kh = keyboard.mOccupiedHeight; + return nearestLettersFromMap(x / kw, y / kh, buildCharToPos(keyboard)); + } + + public static boolean isSequenceMatch(String word, float[] path, float[][] charToPos) { + int n = path.length / 2; + int pathIdx = 0; + char lastChar = 0; + String w = word.toLowerCase(Locale.ROOT); + for (int i = 0; i < w.length(); i++) { + char c = w.charAt(i); + if (c < 'a' || c > 'z') continue; + if (c == lastChar) continue; + float[] target = charToPos[c - 'a']; + if (target[0] == 0f && target[1] == 0f) continue; + boolean found = false; + while (pathIdx < n) { + float dx = path[2 * pathIdx] - target[0]; + float dy = path[2 * pathIdx + 1] - target[1]; + if (dx * dx + dy * dy <= 0.05f) { found = true; break; } + pathIdx++; + } + if (!found) return false; + lastChar = c; + } + return true; + } + + public static SuggestionResults rankByIndex( + GestureIndex index, + InputPointers pointers, + Keyboard keyboard, + int maxResults, + java.util.Set predictionSet + ) { + int n = pointers.getPointerSize(); + SuggestionResults empty = new SuggestionResults(1, false, false); + if (n < 2 || index == null) return empty; + + int[] xs = pointers.getXCoordinates(); + int[] ys = pointers.getYCoordinates(); + float kw = keyboard.mOccupiedWidth, kh = keyboard.mOccupiedHeight; + + // ponytail: use charToPos from index — already built, no reallocation + float[][] charToPos = index.charToPos; + + List startLetters = nearestLettersFromMap(xs[0] / kw, ys[0] / kh, charToPos); + List endLetters = nearestLettersFromMap(xs[n-1] / kw, ys[n-1] / kh, charToPos); + + List candidates = new ArrayList<>(); + for (char first : startLetters) { + List list = index.byFirst.get(first); + if (list != null) candidates.addAll(list); + } + if (candidates.isEmpty()) return empty; + + // ponytail: build flat input path inline, no ArrayList allocation + float[] rawFlat = new float[n * 2]; + for (int i = 0; i < n; i++) { + rawFlat[2 * i] = xs[i] / kw; + rawFlat[2 * i + 1] = ys[i] / kh; + } + float[] inputVec = resampleFlat(rawFlat, n, N_PTS); + float inputLength = pathLength(inputVec); + + // Filter by last letter first; relax if empty + List filtered = new ArrayList<>(candidates.size()); + for (IndexEntry e : candidates) { + String w = e.word.toLowerCase(Locale.ROOT); + if (!w.isEmpty() && endLetters.contains(w.charAt(w.length() - 1))) + filtered.add(e); + } + if (filtered.isEmpty()) filtered = candidates; + + int m = filtered.size(); + + // ponytail: parallel float[] + int[] sort avoids Integer boxing + float[] scores = new float[m]; + int[] order = new int[m]; + for (int i = 0; i < m; i++) { + IndexEntry e = filtered.get(i); + float freqBonus = (e.frequency > 0) ? (float)(Math.log(e.frequency + 1) * FREQ_WEIGHT) : 0f; + boolean seqMatch = isSequenceMatch(e.word, inputVec, charToPos); + float seqPenalty = seqMatch ? 0f : -0.4f; + boolean isPredicted = predictionSet != null && predictionSet.contains(e.word.toLowerCase(Locale.ROOT)); + float predBonus = isPredicted ? 0.15f : 0f; + // ponytail: use precomputed pathLen from IndexEntry instead of recalculating + float lenPenalty = -Math.abs(inputLength - e.pathLen) * 0.4f; + // ponytail: apply self-learning user boost at ranking time too + String lk = e.word.toLowerCase(Locale.ROOT); + Integer ub = sUserBoost.get(lk); + float userBonus = ub != null ? (float) Math.log(ub + 1) * 0.08f : 0f; + scores[i] = -l2(inputVec, e.path) + freqBonus + seqPenalty + predBonus + lenPenalty + userBonus; + order[i] = i; + } + + // ponytail: primitive int sort with insertion sort for small N (fast for <500 items) + for (int i = 1; i < m; i++) { + int key = order[i]; + float ks = scores[key]; + int j = i - 1; + while (j >= 0 && scores[order[j]] < ks) { + order[j + 1] = order[j]; + j--; + } + order[j + 1] = key; + } + + int take = Math.min(maxResults, m); + SuggestionResults result = new SuggestionResults(take, false, false); + int baseScore = 1_000_000; + for (int rank = 0; rank < take; rank++) { + IndexEntry e = filtered.get(order[rank]); + result.add(new SuggestedWordInfo( + e.word, "", + baseScore - rank * 1000, + SuggestedWordInfo.KIND_CORRECTION, + Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX, + SuggestedWordInfo.NOT_A_CONFIDENCE + )); + } + return result; + } + + // ── Internals ───────────────────────────────────────────────────────────── + + private static float pathLength(float[] path) { + float len = 0; + int n = path.length / 2; + for (int i = 0; i < n - 1; i++) { + float dx = path[2 * (i + 1)] - path[2 * i]; + float dy = path[2 * (i + 1) + 1] - path[2 * i + 1]; + len += (float) Math.sqrt(dx * dx + dy * dy); + } + return len; + } + + static float[][] buildCharToPos(Keyboard keyboard) { + float[][] map = new float[26][2]; + float kw = keyboard.mOccupiedWidth, kh = keyboard.mOccupiedHeight; + for (Key key : keyboard.getSortedKeys()) { + int code = key.getCode(); + if (!isAsciiLetter(code)) continue; + int idx = Character.toLowerCase((char) code) - 'a'; + map[idx][0] = (key.getX() + key.getWidth() / 2f) / kw; + map[idx][1] = (key.getY() + key.getHeight() / 2f) / kh; + } + return map; + } + + static float[] wordPath(String word, float[][] charToPos) { + float[] pts = new float[word.length() * 2]; + int count = 0; + float lastX = -1f, lastY = -1f; + for (int i = 0; i < word.length(); i++) { + char c = word.charAt(i); + int idx = c - 'a'; + if (idx < 0 || idx >= 26) continue; + float[] p = charToPos[idx]; + if (count == 0 || p[0] != lastX || p[1] != lastY) { + pts[2 * count] = p[0]; + pts[2 * count + 1] = p[1]; + lastX = p[0]; lastY = p[1]; + count++; + } + } + return resampleFlat(pts, count, N_PTS); + } + + static float[] resampleFlat(float[] pts, int numPts, int n) { + if (numPts == 0) return new float[n * 2]; + if (numPts == 1) { + float[] r = new float[n * 2]; + float x = pts[0], y = pts[1]; + for (int i = 0; i < n; i++) { r[2*i] = x; r[2*i+1] = y; } + return r; + } + float[] cum = new float[numPts]; + for (int i = 1; i < numPts; i++) { + float dx = pts[2 * i] - pts[2 * (i - 1)]; + float dy = pts[2 * i + 1] - pts[2 * (i - 1) + 1]; + cum[i] = cum[i-1] + (float) Math.sqrt(dx*dx + dy*dy); + } + float total = cum[numPts-1]; + if (total < 1e-9f) { + float[] r = new float[n * 2]; + float x = pts[0], y = pts[1]; + for (int i = 0; i < n; i++) { r[2*i] = x; r[2*i+1] = y; } + return r; + } + float[] result = new float[n * 2]; + int seg = 0; + for (int i = 0; i < n; i++) { + float t = total * i / (n - 1); + while (seg < numPts - 2 && cum[seg + 1] < t) seg++; + float segLen = cum[seg+1] - cum[seg]; + float alpha = (segLen > 1e-9f) ? (t - cum[seg]) / segLen : 0f; + result[2*i] = pts[2 * seg] + alpha * (pts[2 * (seg + 1)] - pts[2 * seg]); + result[2*i+1] = pts[2 * seg + 1] + alpha * (pts[2 * (seg + 1) + 1] - pts[2 * seg + 1]); + } + return result; + } + + // ponytail: kept for compat, delegates to resampleFlat + static float[] resample(List pts, int n) { + float[] flat = new float[pts.size() * 2]; + for (int i = 0; i < pts.size(); i++) { + flat[2*i] = pts.get(i)[0]; + flat[2*i+1] = pts.get(i)[1]; + } + return resampleFlat(flat, pts.size(), n); + } + + private static float l2(float[] a, float[] b) { + float s = 0; + int n = a.length / 2; + for (int i = 0; i < n; i++) { + float dx = a[2 * i] - b[2 * i]; + float dy = a[2 * i + 1] - b[2 * i + 1]; + float distSq = dx * dx + dy * dy; + // ponytail: weight endpoints twice — more precisely typed + if (i == 0 || i == n - 1) s += distSq * 2.0f; + else s += distSq; + } + return (float) Math.sqrt(s); + } +} 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 9e3ad6d93..befa2ba39 100644 --- a/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingLoader.kt +++ b/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingLoader.kt @@ -27,12 +27,9 @@ object HandwritingLoader { apkFile.setReadOnly() try { - val md5 = java.security.MessageDigest.getInstance("MD5") - val bytes = apkFile.readBytes() - val hash = md5.digest(bytes).joinToString("") { "%02x".format(it) } - Log.i("HandwritingLoader", "Loaded plugin APK path: ${apkFile.absolutePath}, size: ${bytes.size}, md5: $hash") + Log.i("HandwritingLoader", "Loaded plugin APK path: ${apkFile.absolutePath}, size: ${apkFile.length()}") } catch (e: Exception) { - Log.e("HandwritingLoader", "Failed to calculate MD5", e) + Log.e("HandwritingLoader", "Failed to log plugin info", e) } try { 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 73a382541..78a5ed0ca 100644 --- a/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingView.kt +++ b/app/src/main/java/helium314/keyboard/latin/handwriting/HandwritingView.kt @@ -364,6 +364,11 @@ class HandwritingView @JvmOverloads constructor( } override fun onLongPressKey(primaryCode: Int) { + if (primaryCode == KeyCode.CLEAR_HANDWRITING) { + PointerTracker.cancelAllPointerTrackers() + KeyboardSwitcher.getInstance().setAlphabetKeyboard() + return + } keyboardActionListener?.onLongPressKey(primaryCode) } 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 46f561678..84957ce88 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -391,7 +391,8 @@ public InputTransaction onTextInput(final SettingsValues settingsValues, final E StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode()); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState - mSpaceState = SpaceState.NONE; + // ponytail: set PHANTOM space state after emoji if autospace after emoji is enabled + mSpaceState = (settingsValues.mAutospaceAfterEmoji && StringUtilsKt.isEmoji(text)) ? SpaceState.PHANTOM : SpaceState.NONE; mEnteredText = text; mWordBeingCorrectedByCursor = null; inputTransaction.setDidAffectContents(); @@ -2928,8 +2929,10 @@ private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event return false; } + // ponytail: only strip auto-inserted spaces (WEAK/PHANTOM/SWAP), never manual (NONE) if (isSpaceStrippingPunctuation(codePoint) - && !inputTransaction.getSettingsValues().isUsuallyPrecededBySpace(codePoint)) { + && !inputTransaction.getSettingsValues().isUsuallyPrecededBySpace(codePoint) + && inputTransaction.getSpaceState() != SpaceState.NONE) { if (mConnection.getCodePointBeforeCursor() == Constants.CODE_SPACE) { mConnection.removeTrailingSpace(); } 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 4fa40c105..15398e7c8 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -41,6 +41,7 @@ object Defaults { LayoutType.SHORTCUT_TOP -> "shortcut_top" LayoutType.SHORTCUT_BOTTOM -> "shortcut_bottom" LayoutType.HANDWRITING_BOTTOM -> "handwriting_bottom_row" + LayoutType.EDITING -> "editing" } const val PREF_SPLIT_TOOLBAR = false @@ -89,6 +90,8 @@ object Defaults { const val PREF_ENABLE_SPLIT_KEYBOARD = false const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false const val PREF_PERSIST_FLOATING_KEYBOARD = false + // ponytail: persist text edit mode default + const val PREF_PERSIST_TEXT_EDIT_MODE = false @JvmField val PREF_SPLIT_SPACER_SCALE = Array(2) { DEFAULT_SIZE_SCALE } @JvmField @@ -109,6 +112,7 @@ object Defaults { const val PREF_SHORTCUT_BOTTOM_ROW = false const val PREF_AUTOSPACE_ENABLED = true const val PREF_AUTOSPACE_AFTER_PUNCTUATION = false + const val PREF_AUTOSPACE_AFTER_EMOJI = false const val PREF_AUTOSPACE_AFTER_SUGGESTION = true const val PREF_AUTOSPACE_AFTER_GESTURE_TYPING = true const val PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = true @@ -121,6 +125,8 @@ object Defaults { const val PREF_COMPRESS_SCREENSHOTS = true const val PREF_AUTO_READ_OTP = false const val PREF_GESTURE_INPUT = true + // ponytail: gesture method default value + const val PREF_GESTURE_METHOD = "fallback" const val PREF_VIBRATION_DURATION_SETTINGS = -1 const val PREF_VIBRATION_AMPLITUDE_SETTINGS = -1 const val PREF_KEYPRESS_SOUND_VOLUME = -0.01f @@ -219,6 +225,7 @@ object Defaults { const val PREF_TOOLBAR_MODE = "EXPANDABLE" const val PREF_TOOLBAR_HIDING_GLOBAL = true const val PREF_QUICK_PIN_TOOLBAR_KEYS = true + const val PREF_TOOLBAR_LONG_PRESS_HINT = true val PREF_PINNED_TOOLBAR_KEYS = defaultPinnedToolbarPref val PREF_TOOLBAR_KEYS = defaultToolbarPref const val PREF_AUTO_SHOW_TOOLBAR = false 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 3755f0b3f..ebbebde33 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -115,6 +115,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // autospace regardless of this pref). Exposed via the AUTOSPACE toolbar toggle. public static final String PREF_AUTOSPACE_ENABLED = "autospace_enabled"; public static final String PREF_AUTOSPACE_AFTER_PUNCTUATION = "autospace_after_punctuation"; + public static final String PREF_AUTOSPACE_AFTER_EMOJI = "autospace_after_emoji"; public static final String PREF_AUTOSPACE_AFTER_SUGGESTION = "autospace_after_suggestion"; public static final String PREF_AUTOSPACE_AFTER_GESTURE_TYPING = "autospace_after_gesture_typing"; public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing"; @@ -124,6 +125,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_SUGGEST_PUNCTUATION = "suggest_punctuation"; public static final String PREF_SUGGEST_CLIPBOARD_CONTENT = "suggest_clipboard_content"; public static final String PREF_GESTURE_INPUT = "gesture_input"; + // ponytail: gesture method preference key + public static final String PREF_GESTURE_METHOD = "gesture_method"; public static final String PREF_VIBRATION_DURATION_SETTINGS = "vibration_duration_settings"; public static final String PREF_VIBRATION_AMPLITUDE_SETTINGS = "vibration_amplitude_settings"; public static final String PREF_KEYPRESS_SOUND_VOLUME = "keypress_sound_volume"; @@ -218,6 +221,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang 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_PERSIST_TEXT_EDIT_MODE = "persist_text_edit_mode"; public static final String PREF_FORCE_AUTO_CAPS = "force_auto_caps"; public static final String PREF_OFFLINE_TEMP = "offline_temp"; public static final String PREF_OFFLINE_TOP_P = "offline_top_p"; @@ -251,6 +255,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_URL_DETECTION = "url_detection"; public static final String PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG = "dont_show_missing_dict_dialog"; public static final String PREF_QUICK_PIN_TOOLBAR_KEYS = "quick_pin_toolbar_keys"; + public static final String PREF_TOOLBAR_LONG_PRESS_HINT = "toolbar_long_press_hint"; public static final String PREF_DISABLE_NETWORK = "disable_network"; public static final String PREF_PINNED_TOOLBAR_KEYS = "pinned_toolbar_keys"; public static final String PREF_TOOLBAR_KEYS = "toolbar_keys"; 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 b4e57e442..d8b6cdc3b 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -93,6 +93,7 @@ public class SettingsValues { // in shouldInsertSpacesAutomatically() so this works alongside the input-type guard. public final boolean mAutospaceEnabled; public final boolean mAutospaceAfterPunctuation; + public final boolean mAutospaceAfterEmoji; public final boolean mAutospaceAfterSuggestion; public final boolean mAutospaceAfterGestureTyping; public final boolean mAutospaceBeforeGestureTyping; @@ -115,6 +116,7 @@ public class SettingsValues { // yet public final boolean mSuggestPunctuation; public final boolean mCenterSuggestionTextToEnter; + public final String mGestureMethod; public final boolean mGestureInputEnabled; public final boolean mGestureTrailEnabled; public final boolean mGestureFloatingPreviewTextEnabled; @@ -202,6 +204,8 @@ public class SettingsValues { public final float mAutoCorrectionThreshold; public final boolean mAutoCorrectShortcuts; public final boolean mPersistFloatingKeyboard; + // ponytail: persist text edit mode field + public final boolean mPersistTextEditMode; public final boolean mBackspaceRevertsAutocorrect; public final int mScoreLimitForAutocorrect; private final boolean mSuggestionsEnabledPerUserSettings; @@ -303,6 +307,9 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_AUTOCORRECT_SHORTCUTS); mPersistFloatingKeyboard = prefs.getBoolean(Settings.PREF_PERSIST_FLOATING_KEYBOARD, Defaults.PREF_PERSIST_FLOATING_KEYBOARD); + // ponytail: load persist text edit mode value + mPersistTextEditMode = prefs.getBoolean(Settings.PREF_PERSIST_TEXT_EDIT_MODE, + Defaults.PREF_PERSIST_TEXT_EDIT_MODE); mBackspaceRevertsAutocorrect = prefs.getBoolean(Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT, Defaults.PREF_BACKSPACE_REVERTS_AUTOCORRECT); mBigramPredictionEnabled = prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, @@ -344,8 +351,11 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_KEYPRESS_SOUND_VOLUME); mEnableEmojiAltPhysicalKey = prefs.getBoolean(Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY, Defaults.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY); + mGestureMethod = prefs.getString(Settings.PREF_GESTURE_METHOD, + JniUtils.sHaveNativeGestureLib ? "native" : "fallback"); mGestureInputEnabled = JniUtils.sHaveGestureLib - && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT); + && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) + && (!"native".equals(mGestureMethod) || JniUtils.sHaveNativeGestureLib); mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, Defaults.PREF_GESTURE_PREVIEW_TRAIL); mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText @@ -456,6 +466,8 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_AUTOSPACE_ENABLED); mAutospaceAfterPunctuation = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION); + mAutospaceAfterEmoji = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_EMOJI, + Defaults.PREF_AUTOSPACE_AFTER_EMOJI); mAutospaceAfterSuggestion = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, Defaults.PREF_AUTOSPACE_AFTER_SUGGESTION); mAutospaceAfterGestureTyping = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_GESTURE_TYPING, @@ -503,7 +515,8 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mNarrowKeyGapsLevel = prefs.getInt(Settings.PREF_NARROW_KEY_GAPS_LEVEL, Defaults.PREF_NARROW_KEY_GAPS_LEVEL); mSettingsValuesForSuggestion = new SettingsValuesForSuggestion( mBlockPotentiallyOffensive, - prefs.getBoolean(Settings.PREF_GESTURE_SPACE_AWARE, Defaults.PREF_GESTURE_SPACE_AWARE)); + prefs.getBoolean(Settings.PREF_GESTURE_SPACE_AWARE, Defaults.PREF_GESTURE_SPACE_AWARE), + mGestureMethod); mSpacingAndPunctuations = new SpacingAndPunctuations(res, mUrlDetectionEnabled); mBottomPaddingScale = Settings.readBottomPaddingScale(prefs, isLandscape); mSidePaddingScale = Settings.readSidePaddingScale(prefs, isLandscape, mIsSplitKeyboardEnabled); diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValuesForSuggestion.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValuesForSuggestion.java index 1aa9af91d..8d204f1d9 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValuesForSuggestion.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValuesForSuggestion.java @@ -8,14 +8,16 @@ public class SettingsValuesForSuggestion { public final boolean mBlockPotentiallyOffensive; + public final boolean mSpaceAwareGesture; + public final String mGestureMethod; public SettingsValuesForSuggestion( final boolean blockPotentiallyOffensive, - final boolean spaceAwareGesture + final boolean spaceAwareGesture, + final String gestureMethod ) { mBlockPotentiallyOffensive = blockPotentiallyOffensive; mSpaceAwareGesture = spaceAwareGesture; + mGestureMethod = gestureMethod; } - - public final boolean mSpaceAwareGesture; } diff --git a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java index 458121f48..207e54293 100644 --- a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java +++ b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java @@ -87,7 +87,7 @@ public void onCreate() { onSharedPreferenceChanged(prefs, Settings.PREF_USE_CONTACTS); onSharedPreferenceChanged(prefs, Settings.PREF_USE_APPS); final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE); - mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false); + mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false, "fallback"); } @Override @@ -113,7 +113,7 @@ public void onSharedPreferenceChanged(final SharedPreferences prefs, final Strin } case Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE -> { final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE); - mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false); + mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false, "fallback"); }} } 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 4b00aba67..b0b04da06 100644 --- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt +++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt @@ -54,6 +54,7 @@ import helium314.keyboard.latin.utils.ToolbarKey import helium314.keyboard.latin.utils.ToolbarMode import helium314.keyboard.latin.utils.addPinnedKey import helium314.keyboard.latin.utils.createToolbarKey +import helium314.keyboard.latin.utils.setToolbarButtonActivatedState import helium314.keyboard.latin.utils.isRepeatableToolbarKey import helium314.keyboard.latin.utils.RepeatableKeyTouchListener import helium314.keyboard.latin.utils.dpToPx @@ -178,7 +179,9 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) val toolbarHeight = min(toolbarExpandKey.layoutParams.height, resources.getDimension(R.dimen.config_suggestions_strip_height).toInt()) toolbarExpandKey.layoutParams.height = toolbarHeight toolbarExpandKey.layoutParams.width = toolbarHeight // we want it square - colors.setBackground(toolbarExpandKey, ColorType.STRIP_BACKGROUND) // necessary because background is re-used for defaultToolbarBackground + toolbarExpandKey.setBackgroundResource(R.drawable.toolbar_key_background) + val expandPadding = 9.dpToPx(resources) + toolbarExpandKey.setPadding(expandPadding, expandPadding, expandPadding, expandPadding) colors.setColor(toolbarExpandKey, ColorType.TOOL_BAR_EXPAND_KEY) colors.setColor(toolbarExpandKey.background, ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND) @@ -620,28 +623,15 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) private fun onLongClickToolbarKey(view: View) { val tag = view.tag as? ToolbarKey ?: return - // Special handling for TRANSLATE key - always allow language selector - if (tag === ToolbarKey.TRANSLATE) { - val longClickCode = getCodeForToolbarKeyLongClick(tag) - if (longClickCode != KeyCode.UNSPECIFIED) { - listener.onCodeInput(longClickCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false) - } - return - } - - // Disable pinning when split toolbar is enabled - if (Settings.getValues().mSplitToolbar || !Settings.getValues().mQuickPinToolbarKeys) { - // Quick Pin disabled or Split Toolbar enabled: Perform standard long-press action - val longClickCode = getCodeForToolbarKeyLongClick(tag) - if (longClickCode != KeyCode.UNSPECIFIED) { - listener.onCodeInput(longClickCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false) - } - } else { + val longClickCode = getCodeForToolbarKeyLongClick(tag) + if (longClickCode != KeyCode.UNSPECIFIED) { + // Always perform long-press shortcut if one exists + listener.onCodeInput(longClickCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false) + } else if (Settings.getValues().mQuickPinToolbarKeys && !Settings.getValues().mSplitToolbar) { + // If no shortcut exists, and quick pin is enabled, perform pinning/unpinning if (view.parent === toolbar) { - // Pin: Move from toolbar to pinned keys addPinnedKey(context.prefs(), tag) } else if (view.parent === pinnedKeys) { - // Unpin: Move from pinned keys back to toolbar removePinnedKey(context.prefs(), tag) } } @@ -854,6 +844,13 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) // Setup close button translateLanguageCloseButton.isVisible = true + translateLanguageCloseButton.setBackgroundResource(R.drawable.toolbar_key_background) + val closePadding = 9.dpToPx(resources) + translateLanguageCloseButton.setPadding(closePadding, closePadding, closePadding, closePadding) + translateLanguageCloseButton.setImageDrawable(KeyboardIconsSet.instance.getNewDrawable(ToolbarKey.CLOSE_HISTORY.name, context)) + val colors = Settings.getValues().mColors + colors.setColor(translateLanguageCloseButton, ColorType.TOOL_BAR_EXPAND_KEY) + colors.setColor(translateLanguageCloseButton.background, ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND) translateLanguageCloseButton.setOnClickListener { hideTranslateLanguageSelector() } @@ -973,10 +970,7 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) view.setOnClickListener(this) view.setOnLongClickListener(this) } - colors.setColor(view, ColorType.TOOL_BAR_KEY) - // Set circular background for toolbar keys - view.setBackgroundResource(R.drawable.toolbar_key_background) - colors.setColor(view.background, ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND) + setToolbarButtonActivatedState(view) } private fun rebuildToolbarKeys() { diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ColorUtil.kt b/app/src/main/java/helium314/keyboard/latin/utils/ColorUtil.kt index d4a55fd93..2d1b45749 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/ColorUtil.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/ColorUtil.kt @@ -39,12 +39,10 @@ private fun getBrightnessSquared(@ColorInt color: Int): Int { @ColorInt fun adjustLuminosityAndKeepAlpha(@ColorInt color: Int, amount: Float): Int { - val alpha = Color.alpha(color) val hsl = FloatArray(3) ColorUtils.colorToHSL(color, hsl) - hsl[2] += amount - val newColor = ColorUtils.HSLToColor(hsl) - return Color.argb(alpha, Color.red(newColor), Color.green(newColor), Color.blue(newColor)) + hsl[2] = (hsl[2] + amount).coerceIn(0f, 1f) + return ColorUtils.setAlphaComponent(ColorUtils.HSLToColor(hsl), Color.alpha(color)) } @ColorInt 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 c66942fe7..cc52af5af 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt @@ -94,8 +94,10 @@ fun getDictionaryLocales(context: Context): MutableSet { if (assetsDictionaryList != null) { for (dictionary in assetsDictionaryList) { val locale = DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary) - val isEnabled = enabledLocales.contains(locale) val hasEnabledLanguage = enabledLocales.any { it.language == locale.language } + // ponytail: only show assets for enabled languages to avoid showing preloaded en-US when not used + if (!hasEnabledLanguage) continue + val isEnabled = enabledLocales.contains(locale) if (!isEnabled && hasEnabledLanguage) continue locales.add(locale) } @@ -240,19 +242,49 @@ private fun hasAnythingOtherThanExtractedMainDictionary(context: Context, dir: F return false } -// ponytail: Dynamic dictionary downloader using HTTP URL connection. +// ponytail: Dynamic dictionary downloader using HTTP URL connection with User-Agent, redirects, and timeouts. fun downloadDictionary(context: Context, locale: Locale, type: String, linkUrl: String, onComplete: (Boolean) -> Unit) { val cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, context) ?: return onComplete(false) val targetFile = File(cacheDir, "${type}.dict") CoroutineScope(Dispatchers.IO).launch { var success = false try { - java.net.URL(linkUrl).openStream().use { input -> - targetFile.outputStream().use { output -> - input.copyTo(output) + var url = java.net.URL(linkUrl) + var connection = url.openConnection() as java.net.HttpURLConnection + connection.setRequestProperty("User-Agent", "HeliboardL/3.8.9 (Android)") + connection.connectTimeout = 15000 + connection.readTimeout = 15000 + connection.instanceFollowRedirects = true + + var status = connection.responseCode + var conn = connection + var redirectCount = 0 + while ((status == java.net.HttpURLConnection.HTTP_MOVED_TEMP || + status == java.net.HttpURLConnection.HTTP_MOVED_PERM || + status == 307 || status == 308) && redirectCount < 5) { + val newUrl = conn.getHeaderField("Location") ?: break + conn.disconnect() + val nextUrl = java.net.URL(newUrl) + conn = nextUrl.openConnection() as java.net.HttpURLConnection + conn.setRequestProperty("User-Agent", "HeliboardL/3.8.9 (Android)") + conn.connectTimeout = 15000 + conn.readTimeout = 15000 + conn.instanceFollowRedirects = true + status = conn.responseCode + redirectCount++ + } + + if (status == java.net.HttpURLConnection.HTTP_OK) { + conn.inputStream.use { input -> + targetFile.outputStream().use { output -> + input.copyTo(output) + } } + success = true + } else { + Log.e("DictionaryUtils", "HTTP error downloading dictionary: $status") } - success = true + conn.disconnect() } catch (e: Exception) { Log.e("DictionaryUtils", "Failed to download dictionary", e) } diff --git a/app/src/main/java/helium314/keyboard/latin/utils/GestureLibraryDownloader.kt b/app/src/main/java/helium314/keyboard/latin/utils/GestureLibraryDownloader.kt index 3af9be51f..6f2f5a7db 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/GestureLibraryDownloader.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/GestureLibraryDownloader.kt @@ -23,7 +23,7 @@ object GestureLibraryDownloader { // Base URL for the gesture library files from the trusted openboard repository // This is the official source referenced in HeliBoard's README - private const val BASE_URL = "https://github.com/erkserkserks/openboard/raw/46fdf2b550035ca69299ce312fa158e7ade36967/app/src/main/jniLibs" + private const val BASE_URL = "https://raw.githubusercontent.com/erkserkserks/openboard/46fdf2b550035ca69299ce312fa158e7ade36967/app/src/main/jniLibs" private const val LIB_NAME = "libjni_latinimegoogle.so" /** diff --git a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java index 182dcd69f..8005819d9 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java +++ b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java @@ -40,18 +40,11 @@ public static String expectedDefaultChecksum() { } public static boolean sHaveGestureLib = false; + public static boolean sHaveNativeGestureLib = false; static { // hardcoded default path, may not work on all phones @SuppressLint("SdCardPath") String filesDir = "/data/data/" + BuildConfig.APPLICATION_ID + "/files"; Application app = App.Companion.getApp(); - if (app == null) { - try { - // try using reflection to get (app)context: https://stackoverflow.com/a/38967293 - // this may not be necessary any more, now that we get the app somewhere else? - app = (Application) Class.forName("android.app.ActivityThread") - .getMethod("currentApplication").invoke(null, (Object[]) null); - } catch (Exception ignored) { } - } if (app != null && app.getFilesDir() != null) // use the actual path if possible filesDir = app.getFilesDir().getAbsolutePath(); @@ -108,6 +101,9 @@ public static String expectedDefaultChecksum() { Log.w(TAG, "Could not load native library " + JNI_LIB_NAME, ul); } } + sHaveNativeGestureLib = sHaveGestureLib; + // We have a Java-side gesture engine (SwipeGestureEngine) that doesn't need the native lib. + sHaveGestureLib = true; } private JniUtils() { diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt index 2e0856b83..fb0e267cf 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt @@ -9,7 +9,7 @@ import java.util.EnumMap enum class LayoutType { MAIN, SYMBOLS, MORE_SYMBOLS, FUNCTIONAL, NUMBER, NUMBER_ROW, NUMPAD, NUMPAD_LANDSCAPE, PHONE, PHONE_SYMBOLS, EMOJI_BOTTOM, CLIPBOARD_BOTTOM, - SHORTCUT_TOP, SHORTCUT_BOTTOM, HANDWRITING_BOTTOM; + SHORTCUT_TOP, SHORTCUT_BOTTOM, HANDWRITING_BOTTOM, EDITING; companion object { fun EnumMap.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY) @@ -41,6 +41,7 @@ enum class LayoutType { SHORTCUT_TOP -> R.string.layout_shortcut_top SHORTCUT_BOTTOM -> R.string.layout_shortcut_bottom HANDWRITING_BOTTOM -> R.string.layout_emoji_bottom_row + EDITING -> R.string.text_edit } fun getMainLayoutFromExtraValue(extraValue: String): String? { diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt b/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt deleted file mode 100644 index 01f4c718b..000000000 --- a/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -package helium314.keyboard.latin.utils - -/** - * Tokenizes strings by groupings of non-space characters, making them iterable. Note that letters, - * punctuations, etc. are all treated the same by this construct. - */ -class SpacedTokens(phrase: String) : Iterable { - private val mPhrase = phrase - private val mLength = phrase.length - private val mStartPos = phrase.indexOfFirst { !Character.isWhitespace(it) } - // the iterator should start at the first non-whitespace character - - override fun iterator() = object : Iterator { - private var startPos = mStartPos - - override fun hasNext(): Boolean { - return startPos < mLength && startPos != -1 - } - - override fun next(): String { - var endPos = startPos - - do if (++endPos >= mLength) break - while (!Character.isWhitespace(mPhrase[endPos])) - val word = mPhrase.substring(startPos, endPos) - - if (endPos < mLength) { - do if (++endPos >= mLength) break - while (Character.isWhitespace(mPhrase[endPos])) - } - startPos = endPos - - return word - } - } -} 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 9df8b7bdd..b5ee4fe48 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt @@ -27,9 +27,7 @@ object TextExpanderUtils { return context.prefs().getBoolean(PREF_IMMEDIATE, false) } - fun getPrefix(context: Context): String { - return "" - } + data class ShortcutEntry( val template: String, 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 23c3b49b7..2a719ca37 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt @@ -43,6 +43,7 @@ import android.graphics.Paint import android.graphics.PixelFormat import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter +import android.graphics.Rect import android.graphics.RectF import android.graphics.Typeface import android.graphics.ColorFilter @@ -62,11 +63,11 @@ private val toolbarPrefScope = CoroutineScope(SupervisorJob() + Dispatchers.Defa fun createToolbarKey(context: Context, key: ToolbarKey): ImageButton { val button = ImageButton(context, null, R.attr.suggestionWordStyle) button.scaleType = ImageView.ScaleType.CENTER_INSIDE - val padding = 6.dpToPx(context.resources) + val padding = 9.dpToPx(context.resources) button.setPadding(padding, padding, padding, padding) button.tag = key button.contentDescription = key.name.lowercase().getStringResourceOrName("", context) - setToolbarButtonActivatedState(button) + button.setBackgroundResource(R.drawable.toolbar_key_background) val index = if (key.name.startsWith("CUSTOM_AI_")) { key.name.removePrefix("CUSTOM_AI_").toIntOrNull() @@ -77,11 +78,23 @@ fun createToolbarKey(context: Context, key: ToolbarKey): ImageButton { context.prefs().getString("pref_custom_ai_tag_$index", "") ?: "" } else "" - if (showTags && tag.isNotBlank()) { - button.setImageDrawable(TagDrawable(tag.take(3).uppercase(Locale.US))) + val rawDrawable = if (showTags && tag.isNotBlank()) { + TagDrawable(tag.take(3).uppercase(Locale.US)) } else { - button.setImageDrawable(KeyboardIconsSet.instance.getNewDrawable(key.name, context)) + KeyboardIconsSet.instance.getNewDrawable(key.name, context) } + + val showLongPressHint = context.prefs() + .getBoolean(Settings.PREF_TOOLBAR_LONG_PRESS_HINT, Defaults.PREF_TOOLBAR_LONG_PRESS_HINT) + val finalDrawable = if (rawDrawable != null && showLongPressHint + && getCodeForToolbarKeyLongClick(key) != KeyCode.UNSPECIFIED + ) { + LongPressHintDrawable(rawDrawable) + } else { + rawDrawable + } + button.setImageDrawable(finalDrawable) + setToolbarButtonActivatedState(button) return button } @@ -165,7 +178,7 @@ class TagDrawable(private val text: String) : Drawable() { private val toolbarStateKeys = EnumSet.of( INCOGNITO, ONE_HANDED, SPLIT, AUTOCORRECT, AUTO_CAP, FORCE_AUTO_CAP, - AUTOSPACE, JOIN_NEXT, FORCE_NEXT_SPACE + AUTOSPACE, JOIN_NEXT, FORCE_NEXT_SPACE, SELECT_MODE ) fun setToolbarButtonsActivatedStateOnPrefChange(buttonsGroup: ViewGroup, key: String?) { @@ -197,7 +210,7 @@ fun setToolbarButtonsActivatedState(buttonsGroup: ViewGroup) { buttonsGroup.forEach { if (it is ImageButton) setToolbarButtonActivatedState(it) } } -private fun setToolbarButtonActivatedState(button: ImageButton) { +fun setToolbarButtonActivatedState(button: ImageButton) { val activated = when (button.tag) { INCOGNITO -> button.context.prefs().getBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, Defaults.PREF_ALWAYS_INCOGNITO_MODE) ONE_HANDED -> Settings.getValues().mOneHandedModeEnabled @@ -212,6 +225,7 @@ private fun setToolbarButtonActivatedState(button: ImageButton) { FORCE_AUTO_CAP -> Settings.getValues().mForceAutoCaps JOIN_NEXT -> OneShotSpaceAction.isJoinNextArmed() FORCE_NEXT_SPACE -> OneShotSpaceAction.isForceNextSpaceArmed() + SELECT_MODE -> helium314.keyboard.keyboard.KeyboardActionListenerImpl.sPersistentSelectionModeActive else -> true } button.isActivated = activated @@ -307,6 +321,7 @@ fun getCodeForToolbarKey(key: ToolbarKey) = Settings.getInstance().getCustomTool SPLIT -> KeyCode.SPLIT_LAYOUT PROOFREAD -> KeyCode.PROOFREAD TRANSLATE -> KeyCode.TRANSLATE + SELECT_MODE -> KeyCode.TOGGLE_SELECTION_MODE CUSTOM_AI_1 -> KeyCode.CUSTOM_AI_1 CUSTOM_AI_2 -> KeyCode.CUSTOM_AI_2 CUSTOM_AI_3 -> KeyCode.CUSTOM_AI_3 @@ -343,7 +358,7 @@ fun getCodeForToolbarKeyLongClick(key: ToolbarKey) = Settings.getInstance().getC 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, 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, + PAGE_UP, PAGE_DOWN, FULL_LEFT, FULL_RIGHT, PAGE_START, PAGE_END, JOIN_NEXT, FORCE_NEXT_SPACE, UNDO_WORD, PROOFREAD, TRANSLATE, SELECT_MODE, 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 } @@ -378,7 +393,7 @@ 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, 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, TEXT_EDIT, 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, SELECT_MODE) } val others = entries.filterNot { it in default || it in excludedKeys } @@ -563,3 +578,65 @@ class RepeatableKeyTouchListener( return false } } + +class LongPressHintDrawable(private val base: Drawable) : Drawable() { + private val hintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.WHITE + } + + init { + bounds = base.bounds + } + + override fun draw(canvas: Canvas) { + base.draw(canvas) + val bounds = bounds + val radius = bounds.height() * 0.05f + val cx = bounds.right.toFloat() - radius * 3f + val cy = bounds.bottom.toFloat() - radius * 3f + hintPaint.color = Settings.getValues().mColors.get(ColorType.CLIPBOARD_PIN) + canvas.drawCircle(cx, cy, radius, hintPaint) + } + + override fun onBoundsChange(bounds: Rect) { + base.bounds = bounds + super.onBoundsChange(bounds) + } + + override fun setAlpha(alpha: Int) { + base.alpha = alpha + hintPaint.alpha = (alpha * 0.5f).toInt() + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + base.colorFilter = colorFilter + } + + override fun setTint(tintColor: Int) { + base.setTint(tintColor) + } + + override fun setTintList(tint: ColorStateList?) { + base.setTintList(tint) + } + + override fun setTintMode(tintMode: PorterDuff.Mode?) { + base.setTintMode(tintMode) + } + + @Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.UNKNOWN", "android.graphics.PixelFormat")) + @Suppress("DEPRECATION") + override fun getOpacity(): Int = base.opacity + + override fun isStateful(): Boolean = base.isStateful + + override fun onStateChange(state: IntArray): Boolean { + return base.setState(state) + } + + override fun getIntrinsicWidth(): Int = base.intrinsicWidth + override fun getIntrinsicHeight(): Int = base.intrinsicHeight + override fun getMinimumWidth(): Int = base.minimumWidth + override fun getMinimumHeight(): Int = base.minimumHeight +} diff --git a/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt b/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt index 542de0860..bc6aad141 100644 --- a/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt +++ b/app/src/main/java/helium314/keyboard/settings/WelcomeWizard.kt @@ -56,6 +56,12 @@ import helium314.keyboard.latin.R import helium314.keyboard.latin.permissions.PermissionsUtil import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.utils.JniUtils +import helium314.keyboard.latin.utils.SubtypeSettings +import helium314.keyboard.settings.dialogs.MultiListPickerDialog +import helium314.keyboard.settings.WithSmallTitle +import helium314.keyboard.settings.DropDownField +import helium314.keyboard.latin.utils.SubtypeLocaleUtils.displayName +import helium314.keyboard.latin.utils.locale import helium314.keyboard.latin.utils.UncachedInputMethodManagerUtils import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.prefs @@ -84,7 +90,7 @@ fun WelcomeWizard( var requiresRestart by rememberSaveable { mutableStateOf(false) } var refreshTrigger by remember { mutableIntStateOf(0) } val scope = rememberCoroutineScope() - + LaunchedEffect(step) { if (step == 2) scope.launch { @@ -96,7 +102,7 @@ fun WelcomeWizard( } val useWideLayout = isWideScreen() val appName = stringResource(ctx.applicationInfo.labelRes) - + @Composable fun bigText() { val resource = if (step == 0) R.string.setup_welcome_title else R.string.setup_steps_title Column(Modifier.padding(bottom = 36.dp), horizontalAlignment = Alignment.CenterHorizontally) { @@ -120,32 +126,32 @@ fun WelcomeWizard( @Composable fun ColumnScope.Step( - currentStep: Int, - title: String, - instruction: String, - actionText: String, - icon: Painter, - action: () -> Unit, - onBack: (() -> Unit)? = null, + currentStep: Int, + title: String, + instruction: String, + actionText: String, + icon: Painter, + action: () -> Unit, + onBack: (() -> Unit)? = null, content: @Composable () -> Unit = {} ) { // Progress indicator Row(Modifier.fillMaxWidth().padding(bottom = 24.dp), horizontalArrangement = Arrangement.SpaceEvenly) { - for (i in 1..8) { + for (i in 1..9) { Box( modifier = Modifier .height(6.dp) .weight(1f) .padding(horizontal = 4.dp) .background( - if (i <= currentStep) MaterialTheme.colorScheme.primary + if (i <= currentStep) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, androidx.compose.foundation.shape.CircleShape ) ) } } - + androidx.compose.material3.ElevatedCard( modifier = Modifier.fillMaxWidth(), ) { @@ -153,12 +159,12 @@ fun WelcomeWizard( Text(title, style = MaterialTheme.typography.titleLarge) Spacer(Modifier.height(8.dp)) Text(instruction, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant) - + Spacer(Modifier.height(16.dp)) content() - + Spacer(Modifier.height(24.dp)) - + // Action Buttons Row( Modifier.fillMaxWidth(), @@ -172,9 +178,9 @@ fun WelcomeWizard( } else { Spacer(Modifier.weight(0.1f)) // Placeholder to maintain spacing } - + Spacer(Modifier.weight(1f)) - + androidx.compose.material3.FilledTonalButton(onClick = action) { Icon(icon, null, Modifier.padding(end = 8.dp).size(20.dp)) Text(actionText) @@ -218,19 +224,144 @@ fun WelcomeWizard( null ) } else if (step == 3) { + var showDialog by remember { mutableStateOf(false) } + val allSubtypes = remember { SubtypeSettings.getAllAvailableSubtypes() } + var enabledSubtypes by remember { mutableStateOf(SubtypeSettings.getEnabledSubtypes(true)) } + + val gestureMethods = listOf( + stringResource(R.string.gesture_method_native) to "native", + stringResource(R.string.gesture_method_fallback) to "fallback" + ) + var selectedMethod by remember { + mutableStateOf( + ctx.prefs().getString( + Settings.PREF_GESTURE_METHOD, + if (JniUtils.sHaveNativeGestureLib) "native" else "fallback" + )!! + ) + } + Step( 3, + "Language & Input Selection", + "Configure your typing languages and choose the gesture typing engine type.", + "Next", + painterResource(R.drawable.sym_keyboard_language_switch), + { step++ }, + { step-- } + ) { + WithSmallTitle("Typing Languages") { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium) + .padding(16.dp) + ) { + Text( + "Enabled Languages:", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(Modifier.height(8.dp)) + enabledSubtypes.forEach { subtype -> + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_setup_check), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp).padding(end = 8.dp) + ) + Text( + text = subtype.displayName(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Spacer(Modifier.height(12.dp)) + androidx.compose.material3.Button( + onClick = { showDialog = true }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Choose Languages") + } + } + } + + if (showDialog) { + MultiListPickerDialog( + onDismissRequest = { showDialog = false }, + items = allSubtypes, + initialSelection = enabledSubtypes, + onConfirmed = { selected -> + selected.forEach { subtype -> + if (subtype !in enabledSubtypes) { + SubtypeSettings.addEnabledSubtype(ctx.prefs(), subtype) + } + } + enabledSubtypes.forEach { subtype -> + if (subtype !in selected && selected.isNotEmpty()) { + SubtypeSettings.removeEnabledSubtype(ctx, subtype) + } + } + enabledSubtypes = SubtypeSettings.getEnabledSubtypes(true) + showDialog = false + }, + getItemName = { it.displayName() } + ) + } + + Spacer(Modifier.height(16.dp)) + + WithSmallTitle("Gesture Typing Engine") { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium) + .padding(16.dp) + ) { + DropDownField( + items = gestureMethods, + selectedItem = gestureMethods.first { it.second == selectedMethod }, + onSelected = { pair -> + selectedMethod = pair.second + ctx.prefs().edit { putString(Settings.PREF_GESTURE_METHOD, pair.second) } + refreshTrigger++ + } + ) { pair -> + Text(pair.first, style = MaterialTheme.typography.bodyLarge) + } + Spacer(Modifier.height(8.dp)) + Text( + text = if (selectedMethod == "native") { + "Note: Native engine provides high performance but requires swypelib to be downloaded in the next step." + } else { + "Note: Pure-Java engine works out of the box (Experimental)." + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } else if (step == 4) { + Step( + 4, "Libraries", "Download emoji and gesture libraries to improve typing and suggestions.", "Next", painterResource(R.drawable.sym_keyboard_language_switch), { step++ }, - null + { step-- } ) { val trigger = refreshTrigger // Force recomposition val locale = helium314.keyboard.latin.RichInputMethodManager.getInstance().currentSubtype.locale val emojiLibInstalled = java.io.File(helium314.keyboard.latin.utils.DictionaryInfoUtils.getCacheDirectoryForLocale(locale, ctx), "emoji_${locale.language}.dict").exists() val gestureLibInstalled = java.io.File(ctx.filesDir, "libjni_latinime.so").exists() || JniUtils.sHaveGestureLib + val showGestureDownload = ctx.prefs().getString(Settings.PREF_GESTURE_METHOD, "fallback") == "native" Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium)) { LoadEmojiLibPreference( @@ -241,24 +372,26 @@ fun WelcomeWizard( Icon(painterResource(R.drawable.ic_setup_check), null, Modifier.align(Alignment.CenterEnd).padding(end = 16.dp), tint = MaterialTheme.colorScheme.primary) } } - Spacer(Modifier.height(8.dp)) - Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium)) { - LoadGestureLibPreference( - title = "Gesture Typing Library", - restartOnSuccess = false, - onSuccess = { - requiresRestart = true - refreshTrigger++ + if (showGestureDownload) { + Spacer(Modifier.height(8.dp)) + Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium)) { + LoadGestureLibPreference( + title = "Gesture Typing Library", + restartOnSuccess = false, + onSuccess = { + requiresRestart = true + refreshTrigger++ + } + ) + if (gestureLibInstalled) { + Icon(painterResource(R.drawable.ic_setup_check), null, Modifier.align(Alignment.CenterEnd).padding(end = 16.dp), tint = MaterialTheme.colorScheme.primary) } - ) - if (gestureLibInstalled) { - Icon(painterResource(R.drawable.ic_setup_check), null, Modifier.align(Alignment.CenterEnd).padding(end = 16.dp), tint = MaterialTheme.colorScheme.primary) } } } - } else if (step == 4) { + } else if (step == 5) { Step( - 4, + 5, "AI Integration", "Select an AI service and provide your API key for advanced proofreading features.", "Next", @@ -289,7 +422,7 @@ fun WelcomeWizard( refreshTrigger++ }) }.Preference() - + when (currentProvider) { helium314.keyboard.latin.utils.ProofreadService.AIProvider.GEMINI -> { helium314.keyboard.settings.Setting(ctx, helium314.keyboard.settings.SettingsWithoutKey.GEMINI_API_KEY, R.string.gemini_api_key_title, R.string.gemini_api_key_summary) { setting -> @@ -325,9 +458,9 @@ fun WelcomeWizard( Text("AI features are not available in this build flavor.", color = MaterialTheme.colorScheme.onSurfaceVariant) } } - } else if (step == 5) { + } else if (step == 6) { Step( - 5, + 6, "Floating Keyboard", "Enable floating keyboard by granting the 'Display over other apps' permission.", "Next", @@ -354,9 +487,9 @@ fun WelcomeWizard( } } } - } else if (step == 6) { + } else if (step == 7) { Step( - 6, + 7, "Screenshot Suggestions", "Suggest recently taken screenshots in the suggestion strip. Note: This permission also allows saving screenshots to the clipboard.", "Next", @@ -392,9 +525,9 @@ fun WelcomeWizard( }.Preference() } } - } else if (step == 7) { + } else if (step == 8) { Step( - 7, + 8, "Keyboard Height", "Adjust the height of the keyboard. Recommended: 77% for more square keys, 100% for taller keys.", "Next", @@ -415,15 +548,15 @@ fun WelcomeWizard( description = { "${(100 * it).toInt()}%" } ) {} }.Preference() - + if (heightSet) { Icon(painterResource(R.drawable.ic_setup_check), null, Modifier.align(Alignment.TopEnd).padding(top = 16.dp, end = 16.dp), tint = MaterialTheme.colorScheme.primary) } } } - } else { // step 8 + } else { // step 9 Step( - 8, + 9, stringResource(R.string.setup_step3_title), stringResource(R.string.setup_step3_instruction, appName), stringResource(R.string.setup_finish_action), @@ -469,14 +602,14 @@ fun Step0(onClick: () -> Unit) { modifier = Modifier.fillMaxSize() ) { Image( - painterResource(R.drawable.setup_welcome_image), + painterResource(R.drawable.setup_welcome_image), contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth().weight(1f) ) - + Spacer(Modifier.height(16.dp)) - + androidx.compose.material3.Button( onClick = onClick, modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp, vertical = 16.dp) 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 2c5a98c5d..b9d7e04c1 100644 --- a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt @@ -80,8 +80,9 @@ fun DictionaryDialog( DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dict) } } - val internalId = best?.let { "main:" + it.substringAfter("_").substringBefore(".") } - val mainPrefKey = "pref_dict_enabled_" + (internalId ?: "main:${locale.language}") + // ponytail: normalize key to match format used by DictionaryFactory (lowercase, replace - with _) + val internalId = best?.let { "main:" + it.substringAfter("_").substringBefore(".").lowercase().replace("-", "_") } + val mainPrefKey = "pref_dict_enabled_" + (internalId ?: "main:${locale.toLanguageTag().lowercase().replace("-", "_")}") val prefs = ctx.prefs() var enabled by remember { mutableStateOf(prefs.getBoolean(mainPrefKey, true)) } @@ -185,6 +186,10 @@ private fun DictionaryDetails(dict: File, onDelete: () -> Unit) { modifier = Modifier.padding(end = 8.dp) ) Text(title, style = MaterialTheme.typography.titleSmall, modifier = Modifier.weight(1f)) + DeleteButton { + dict.delete() + onDelete() + } ExpandButton { showDetails = !showDetails } } AnimatedVisibility(showDetails, enter = fadeIn(), exit = fadeOut()) { diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/SponsorDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/SponsorDialog.kt index 90e685068..7a7dac9cb 100644 --- a/app/src/main/java/helium314/keyboard/settings/dialogs/SponsorDialog.kt +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/SponsorDialog.kt @@ -83,41 +83,11 @@ fun SponsorDialog( Column( modifier = Modifier.fillMaxWidth() ) { - // Gradient Header - Box( - modifier = Modifier - .fillMaxWidth() - .height(140.dp) - .background( - Brush.linearGradient( - colors = listOf( - Color(0xFF7C4DFF), // LeanBitLab Purple - Color(0xFFFE8E86) // Sponsor Warm Pink/Rose - ) - ) - ), - contentAlignment = Alignment.Center - ) { - // White glow/background for heart - Box( - modifier = Modifier - .size(84.dp) - .clip(CircleShape) - .background(Color.White.copy(alpha = 0.2f)), - contentAlignment = Alignment.Center - ) { - Text( - "❤️", - style = MaterialTheme.typography.displayMedium - ) - } - } - // Content Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 20.dp), + .padding(horizontal = 24.dp, vertical = 24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -169,46 +139,46 @@ fun SponsorDialog( Spacer(modifier = Modifier.height(16.dp)) - // Buttons Row - Row( + // Buttons Column + Column( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + horizontalAlignment = Alignment.CenterHorizontally ) { - TextButton( + Button( onClick = { if (neverShowAgain) prefs.edit { putBoolean(Settings.PREF_DONT_SHOW_SPONSOR_DIALOG, true) } - onDismissRequest() + onSponsor() }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ), + contentPadding = PaddingValues(vertical = 12.dp) ) { + Text("💖", modifier = Modifier.padding(end = 8.dp)) Text( - text = stringResource(R.string.sponsor_dialog_not_now), - fontWeight = FontWeight.SemiBold + text = "Sponsor on GitHub", + fontWeight = FontWeight.Bold ) } - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - Button( + TextButton( onClick = { if (neverShowAgain) prefs.edit { putBoolean(Settings.PREF_DONT_SHOW_SPONSOR_DIALOG, true) } - onSponsor() + onDismissRequest() }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ), - contentPadding = PaddingValues(horizontal = 20.dp, vertical = 10.dp) + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) ) { - Text("💖", modifier = Modifier.padding(end = 6.dp)) Text( - text = stringResource(R.string.sponsor_dialog_sponsor), - fontWeight = FontWeight.Bold + text = stringResource(R.string.sponsor_dialog_not_now), + fontWeight = FontWeight.SemiBold ) } } diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/LoadEmojiLibPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/LoadEmojiLibPreference.kt index 3186191ca..ef9161399 100644 --- a/app/src/main/java/helium314/keyboard/settings/preferences/LoadEmojiLibPreference.kt +++ b/app/src/main/java/helium314/keyboard/settings/preferences/LoadEmojiLibPreference.kt @@ -76,6 +76,10 @@ fun LoadEmojiLibPreference( val urlStr = "${Links.DICTIONARY_URL}${Links.DICTIONARY_DOWNLOAD_SUFFIX}${Links.DICTIONARY_EMOJI_CLDR_SUFFIX}$dictName" val url = URL(urlStr) val conn = url.openConnection() as HttpURLConnection + conn.setRequestProperty("User-Agent", "HeliboardL/3.8.9 (Android)") + conn.connectTimeout = 15000 + conn.readTimeout = 15000 + conn.instanceFollowRedirects = true conn.connect() if (conn.responseCode != HttpURLConnection.HTTP_OK) { diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt index bf9ccb66f..4e0c7eb32 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt @@ -176,6 +176,7 @@ fun createAboutSettings(context: Context) = listOf( icon = R.drawable.ic_settings_about_github ) }, + Setting(context, SettingsWithoutKey.SAVE_LOG, R.string.save_log) { setting -> val ctx = LocalContext.current val scope = rememberCoroutineScope() 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 372ea599f..dd432d6b9 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt @@ -53,6 +53,7 @@ import helium314.keyboard.settings.SearchSettingsScreen import helium314.keyboard.settings.SettingsActivity import helium314.keyboard.settings.SettingsDestination import helium314.keyboard.settings.dialogs.TextInputDialog +import helium314.keyboard.settings.dialogs.ListPickerDialog import helium314.keyboard.settings.preferences.SliderPreference import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.Theme @@ -689,16 +690,66 @@ fun createAdvancedSettings(context: Context) = listOfNotNull( default = Defaults.PREF_OFFLINE_SHOW_THINKING ) + // ponytail: custom max tokens option + val prefs = context.prefs() + var maxTokens by remember { mutableStateOf(prefs.getInt(Settings.PREF_OFFLINE_MAX_TOKENS, Defaults.PREF_OFFLINE_MAX_TOKENS)) } + var showListDialog by rememberSaveable { mutableStateOf(false) } + var showCustomDialog by rememberSaveable { mutableStateOf(false) } + val tokenEntries = context.resources.getStringArray(R.array.offline_max_tokens_entries) val tokenValues = context.resources.getStringArray(R.array.offline_max_tokens_values).map { it.toInt() } val tokenItems = tokenEntries.zip(tokenValues) - val maxTokenSetting = Setting(context, Settings.PREF_OFFLINE_MAX_TOKENS, R.string.offline_max_tokens_title, R.string.offline_max_tokens_summary) { } - ListPreference( - setting = maxTokenSetting, - items = tokenItems, - default = Defaults.PREF_OFFLINE_MAX_TOKENS + val currentItem = tokenItems.firstOrNull { it.second == maxTokens } + val description = currentItem?.first ?: context.getString(R.string.offline_max_tokens_custom_desc, maxTokens) + + Preference( + name = context.getString(R.string.offline_max_tokens_title), + description = description, + onClick = { showListDialog = true } ) + + val dialogItems = tokenItems + (context.getString(R.string.offline_max_tokens_custom_option) to -1) + + if (showListDialog) { + ListPickerDialog( + onDismissRequest = { showListDialog = false }, + items = dialogItems, + onItemSelected = { + showListDialog = false + if (it.second == -1) { + showCustomDialog = true + } else { + maxTokens = it.second + prefs.edit().putInt(Settings.PREF_OFFLINE_MAX_TOKENS, it.second).apply() + } + }, + selectedItem = currentItem ?: dialogItems.last(), + title = { Text(context.getString(R.string.offline_max_tokens_title)) }, + getItemName = { it.first } + ) + } + + if (showCustomDialog) { + TextInputDialog( + onDismissRequest = { showCustomDialog = false }, + onConfirmed = { text -> + showCustomDialog = false + val value = text.toIntOrNull() + if (value != null && value > 0) { + maxTokens = value + prefs.edit().putInt(Settings.PREF_OFFLINE_MAX_TOKENS, value).apply() + } + }, + title = { Text(context.getString(R.string.offline_max_tokens_title)) }, + initialText = if (maxTokens !in tokenValues) maxTokens.toString() else "", + keyboardType = androidx.compose.ui.text.input.KeyboardType.Number, + checkTextValid = { text -> + val value = text.toIntOrNull() + value != null && value > 0 + } + ) + } } } else null ) // Close listOfNotNull diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt index b790e8609..9421483f9 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt @@ -67,6 +67,8 @@ fun AppearanceScreen( SettingsWithoutKey.BACKGROUND_IMAGE_LANDSCAPE, R.string.settings_category_miscellaneous, Settings.PREF_PERSIST_FLOATING_KEYBOARD, + // ponytail: persist text edit mode settings item + Settings.PREF_PERSIST_TEXT_EDIT_MODE, Settings.PREF_ENABLE_SPLIT_KEYBOARD, Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE, if (prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE, Defaults.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE) @@ -205,6 +207,10 @@ fun createAppearanceSettings(context: Context) = listOf( Setting(context, Settings.PREF_PERSIST_FLOATING_KEYBOARD, R.string.persist_floating_keyboard_title, R.string.persist_floating_keyboard_summary) { SwitchPreference(it, Defaults.PREF_PERSIST_FLOATING_KEYBOARD) }, + // ponytail: persist text edit mode preference widget + Setting(context, Settings.PREF_PERSIST_TEXT_EDIT_MODE, R.string.persist_text_edit_mode_title, R.string.persist_text_edit_mode_summary) { + SwitchPreference(it, Defaults.PREF_PERSIST_TEXT_EDIT_MODE) + }, Setting(context, Settings.PREF_SPLIT_SPACER_SCALE_PREFIX, R.string.split_spacer_scale) { setting -> MultiSliderPreference( name = setting.title, diff --git a/app/src/main/java/helium314/keyboard/settings/screens/BlockedWordsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/BlockedWordsScreen.kt index e363c7db1..de9c1357c 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/BlockedWordsScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/BlockedWordsScreen.kt @@ -123,7 +123,10 @@ fun BlockedWordsScreen( Box(Modifier.fillMaxSize()) { SearchScreen( onClickBack = onClickBack, - title = { Text(stringResource(R.string.edit_blocked_words)) }, + title = { + // ponytail: show total count of blocked words in title + Text("${stringResource(R.string.edit_blocked_words)} (${blockedWords.size})") + }, menu = listOf( stringResource(R.string.clear_all) to { showClearAllDialog = true } ), diff --git a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt index f2680802f..b3760286b 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt @@ -104,8 +104,15 @@ fun ColorsScreen( userColors, ctx, isNight, null ) val allColors = KeyboardTheme.readUserAllColors(prefs, newThemeName.text, fallbackColors) - ColorType.entries.map { - ColorSetting(it.name, null, allColors[it] ?: it.default()) + ColorType.entries.map { ct -> + val cs = ColorSetting(ct.name, null, allColors[ct] ?: ct.default()) + val resId = colorPrefsAndResIds.firstOrNull { it.first == ct.name }?.second + if (resId != null) { + cs.displayName = ctx.getString(resId) + } else { + cs.displayName = ct.name.lowercase().replace('_', ' ').replaceFirstChar { it.uppercase() } + } + cs } } else { val toDisplay = colorPrefsAndResIds.map { (colorName, resId) -> 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 a51cc450d..2ab69070e 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt @@ -458,7 +458,12 @@ fun getUserAndInternalDictionaries(context: Context, locale: Locale): Pair Unit, ) { val context = LocalContext.current - val gestureInstalled = JniUtils.sHaveGestureLib + val gestureInstalled = JniUtils.sHaveNativeGestureLib SearchSettingsScreen( onClickBack = onClickBack, diff --git a/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt index 59dbce68d..260557a4e 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt @@ -76,6 +76,7 @@ fun TextCorrectionScreen( R.string.settings_category_space, Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, + Settings.PREF_AUTOSPACE_AFTER_EMOJI, Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, if (gestureEnabled && !manualGestureSpacing) Settings.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING else null, if (gestureEnabled && !manualGestureSpacing) Settings.PREF_AUTOSPACE_AFTER_GESTURE_TYPING else null, @@ -158,6 +159,11 @@ fun createCorrectionSettings(context: Context) = listOf( ) { SwitchPreference(it, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION) }, + Setting(context, Settings.PREF_AUTOSPACE_AFTER_EMOJI, + R.string.autospace_after_emoji, R.string.autospace_after_emoji_summary + ) { + SwitchPreference(it, Defaults.PREF_AUTOSPACE_AFTER_EMOJI) + }, Setting(context, Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, R.string.autospace_after_suggestion) { SwitchPreference(it, Defaults.PREF_AUTOSPACE_AFTER_SUGGESTION) }, 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 e57298f1f..1aba20bd9 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/ToolbarScreen.kt @@ -72,12 +72,11 @@ fun ToolbarScreen( Settings.PREF_TOOLBAR_MODE, Settings.PREF_SPLIT_TOOLBAR, if (toolbarMode == ToolbarMode.HIDDEN) Settings.PREF_TOOLBAR_HIDING_GLOBAL else null, - if (toolbarMode in listOf(ToolbarMode.EXPANDABLE, ToolbarMode.TOOLBAR_KEYS)) - Settings.PREF_TOOLBAR_KEYS else null, - if (toolbarMode in listOf(ToolbarMode.EXPANDABLE, ToolbarMode.SUGGESTION_STRIP) && !isSplitToolbar) - Settings.PREF_PINNED_TOOLBAR_KEYS else null, - if (clipboardToolbarVisible) Settings.PREF_CLIPBOARD_TOOLBAR_KEYS else null, - if (clipboardToolbarVisible) Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES else null, + Settings.PREF_TOOLBAR_KEYS, + if (!isSplitToolbar) Settings.PREF_PINNED_TOOLBAR_KEYS else null, + Settings.PREF_CLIPBOARD_TOOLBAR_KEYS, + Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, + Settings.PREF_TOOLBAR_LONG_PRESS_HINT, if (toolbarMode == ToolbarMode.EXPANDABLE && !isSplitToolbar) Settings.PREF_QUICK_PIN_TOOLBAR_KEYS else null, if (toolbarMode == ToolbarMode.EXPANDABLE && !isSplitToolbar) Settings.PREF_AUTO_SHOW_TOOLBAR else null, if (toolbarMode == ToolbarMode.EXPANDABLE && !isSplitToolbar) Settings.PREF_AUTO_SHOW_TOOLBAR_ON_SELECT else null, @@ -149,6 +148,11 @@ fun createToolbarSettings(context: Context): List { { SwitchPreference(it, Defaults.PREF_QUICK_PIN_TOOLBAR_KEYS) { KeyboardSwitcher.getInstance().setThemeNeedsReload() } }, + Setting(context, Settings.PREF_TOOLBAR_LONG_PRESS_HINT, + R.string.toolbar_long_press_hint, R.string.toolbar_long_press_hint_summary) + { + SwitchPreference(it, Defaults.PREF_TOOLBAR_LONG_PRESS_HINT) { KeyboardSwitcher.getInstance().setThemeNeedsReload() } + }, Setting(context, Settings.PREF_AUTO_SHOW_TOOLBAR, R.string.auto_show_toolbar_open, R.string.auto_show_toolbar_summary) { SwitchPreference(it, Defaults.PREF_AUTO_SHOW_TOOLBAR) diff --git a/app/src/main/res/layout/main_keyboard_frame.xml b/app/src/main/res/layout/main_keyboard_frame.xml index 2123eeed6..fcfd39ace 100644 --- a/app/src/main/res/layout/main_keyboard_frame.xml +++ b/app/src/main/res/layout/main_keyboard_frame.xml @@ -43,11 +43,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" /> - + diff --git a/app/src/main/res/layout/text_edit_view.xml b/app/src/main/res/layout/text_edit_view.xml deleted file mode 100644 index 21ee0a279..000000000 --- a/app/src/main/res/layout/text_edit_view.xml +++ /dev/null @@ -1,236 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f5aa12ee..388fc1b1e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -438,6 +438,12 @@ Load gesture typing library Provide a native library to enable gesture typing + + + Gesture typing method + Choose algorithm for swipe typing + Native library (requires swypelib) + Fallback engine (pure Java) You will need the library for \'%s\'. Incompatible libraries may crash when using gesture typing. \n\nWarning: loading external code can be a security risk. Only use a library from a source you trust. @@ -473,6 +479,10 @@ Autospace after punctuation Automatically insert space after punctuation when typing a new word + + Autospace after emoji + + Automatically insert space after emoji when typing a new word Autospace after picking a suggestion @@ -506,6 +516,7 @@ Force next space Undo word Text editing + Select mode Full-screen touchpad Hide suggestion strip and toolbar when touchpad mode is active @@ -791,6 +802,10 @@ Pin toolbar key on long press This will disable other long press actions for toolbar keys that are not pinned + + Show long-press hint dots + + Show dots on toolbar keys that have a long-press action Show functional hints @@ -1416,6 +1431,8 @@ New dictionary: Manage local LLM Offline AI Max Tokens Maximum length of the generated correction + Custom… + Custom (%1$d tokens) Temperature Controls randomness: lower is more focused and deterministic Top-P (Nucleus Sampling) @@ -1450,4 +1467,7 @@ New dictionary: Drag to resize keyboard Persist floating keyboard Do not hide floating keyboard when input finishes + + Persist text editing mode + Do not exit text editing mode when input finishes diff --git a/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt b/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt index 5e5ea4d9f..0d129ad24 100644 --- a/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt @@ -276,6 +276,25 @@ class SuggestTest { assertEquals("word'", result.mWord) } + @Test fun `misspelled word is corrected using relaxed threshold even with low score`() { + val locale = Locale.ENGLISH + // typed word: "recpa" (length 5) -> not in dictionary + // suggestion: "recep" (score 300,000) + // edit distance is 2, normalizedScore is 0.3 * (1 - 2/5) = 0.18 + // 0.18 < 0.185 (threshold), but adjustedThreshold is 0.185 * (3/5) = 0.111 + // Since score 300,000 > scoreLimit / 4 (237,500) and length > 3, it should correct! + val result = shouldBeAutoCorrected( + "recpa", + listOf(suggestion("recep", 300000, locale)), + null, + null, + locale, + thresholdModest + ) + assert(result.last()) // should be corrected + } + + private fun shouldBeAutoCorrected(word: String, // typed word suggestions: List, // suggestions ordered by score, including suggestion for typed word if in dictionary firstSuggestionForEmpty: SuggestedWordInfo?, // first suggestion if typed word would be empty (null if none) diff --git a/app/src/test/java/helium314/keyboard/latin/utils/SpacedTokensTest.kt b/app/src/test/java/helium314/keyboard/latin/utils/SpacedTokensTest.kt index 60606cc48..1a56a45ca 100644 --- a/app/src/test/java/helium314/keyboard/latin/utils/SpacedTokensTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/utils/SpacedTokensTest.kt @@ -1,5 +1,6 @@ package helium314.keyboard.latin.utils +import helium314.keyboard.latin.common.splitOnWhitespace import org.junit.Assert.assertEquals import org.junit.Test @@ -7,67 +8,67 @@ class SpacedTokensTest { @Test fun `empty string returns empty list`() { - val tokens = SpacedTokens("").toList() + val tokens = "".splitOnWhitespace() assertEquals(0, tokens.size) } @Test fun `string with only spaces returns empty list`() { - val tokens = SpacedTokens(" ").toList() + val tokens = " ".splitOnWhitespace() assertEquals(0, tokens.size) } @Test fun `string with one token without spaces returns one token`() { - val tokens = SpacedTokens("word").toList() + val tokens = "word".splitOnWhitespace() assertEquals(listOf("word"), tokens) } @Test fun `string with multiple tokens separated by single spaces returns tokens`() { - val tokens = SpacedTokens("this is a test").toList() + val tokens = "this is a test".splitOnWhitespace() assertEquals(listOf("this", "is", "a", "test"), tokens) } @Test fun `string with multiple tokens separated by multiple spaces returns tokens`() { - val tokens = SpacedTokens("this is a test").toList() + val tokens = "this is a test".splitOnWhitespace() assertEquals(listOf("this", "is", "a", "test"), tokens) } @Test fun `string with leading spaces returns tokens`() { - val tokens = SpacedTokens(" leading").toList() + val tokens = " leading".splitOnWhitespace() assertEquals(listOf("leading"), tokens) } @Test fun `string with trailing spaces returns tokens`() { - val tokens = SpacedTokens("trailing ").toList() + val tokens = "trailing ".splitOnWhitespace() assertEquals(listOf("trailing"), tokens) } @Test fun `string with leading and trailing spaces returns tokens`() { - val tokens = SpacedTokens(" both ").toList() + val tokens = " both ".splitOnWhitespace() assertEquals(listOf("both"), tokens) } @Test fun `string with different types of whitespace returns tokens`() { - val tokens = SpacedTokens("token1\ttoken2\ntoken3\rtoken4").toList() + val tokens = "token1\ttoken2\ntoken3\rtoken4".splitOnWhitespace() assertEquals(listOf("token1", "token2", "token3", "token4"), tokens) } @Test fun `string with punctuations as tokens returns tokens`() { - val tokens = SpacedTokens("word1, word2!").toList() + val tokens = "word1, word2!".splitOnWhitespace() assertEquals(listOf("word1,", "word2!"), tokens) } @Test fun `string with emojis as tokens returns tokens`() { - val tokens = SpacedTokens("hello 🌍!").toList() + val tokens = "hello 🌍!".splitOnWhitespace() assertEquals(listOf("hello", "🌍!"), tokens) } diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 30451afb7..38db92813 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -93,6 +93,12 @@ class SettingsContainerTest { assertEquals("Delete last fragment", context.getString(R.string.two_thumb_backspace_fragment)) } + @Test + fun autospaceAfterEmojiSettingIsRegistered() { + assertEquals(Settings.PREF_AUTOSPACE_AFTER_EMOJI, + container[Settings.PREF_AUTOSPACE_AFTER_EMOJI]?.key) + } + @Test fun touchpadEdgeScrollSettingIsRegistered() { assertEquals(Settings.PREF_TOUCHPAD_EDGE_SCROLL, diff --git a/docs/badges/download.svg b/docs/badges/download.svg index cb82e0b80..77090f9ec 100644 --- a/docs/badges/download.svg +++ b/docs/badges/download.svg @@ -1 +1 @@ -VersionVersionv3.8.9v3.8.9 +VersionVersionv3.9.0v3.9.0 diff --git a/docs/badges/downloads.svg b/docs/badges/downloads.svg index 720b811be..5a96ab422 100644 --- a/docs/badges/downloads.svg +++ b/docs/badges/downloads.svg @@ -1 +1 @@ -DownloadsDownloads3350333503 +DownloadsDownloads3523435234 diff --git a/docs/badges/stars.svg b/docs/badges/stars.svg index 74a51bf4d..ff3982034 100644 --- a/docs/badges/stars.svg +++ b/docs/badges/stars.svg @@ -1 +1 @@ -StarsStars502502 +StarsStars517517