use CombiningRules extra value for setting the combiner

allows flexibility which is needed for the combiner suggested in GH-214
This commit is contained in:
Helium314 2025-06-07 21:44:14 +02:00
parent 48f6c21b57
commit ccc287c4ea
9 changed files with 33 additions and 72 deletions

View file

@ -29,30 +29,18 @@ import java.util.*
* cursor: we'll start after this. * cursor: we'll start after this.
* @param initialText The text that has already been combined so far. * @param initialText The text that has already been combined so far.
*/ */
class CombinerChain(initialText: String) { class CombinerChain(initialText: String, combiningSpec: String) {
// The already combined text, as described above // The already combined text, as described above
private val mCombinedText = StringBuilder(initialText) private val mCombinedText = StringBuilder(initialText)
// The feedback on the composing state, as described above // The feedback on the composing state, as described above
private val mStateFeedback = SpannableStringBuilder() private val mStateFeedback = SpannableStringBuilder()
private val mCombiners = ArrayList<Combiner>() private val mCombiners = ArrayList<Combiner>()
// Hangul combiner affects other scripts, e.g. period is seen as port of a word for latin,
// so we need to remove the combiner when not writing in hangul script.
// Maybe it would be better to always have the Hangul combiner, but make sure it doesn't affect
// events for other scripts, but how?
// todo: this really should be done properly, hangul combiner should do nothing when it's not needed
var isHangul = false
set(value) {
if (field == value) return
field = value
if (!value)
mCombiners.removeAll { it is HangulCombiner }
else if (mCombiners.none { it is HangulCombiner })
mCombiners.add(HangulCombiner())
}
init { init {
// The dead key combiner is always active, and always first // The dead key combiner is always active, and always first
mCombiners.add(DeadKeyCombiner()) mCombiners.add(DeadKeyCombiner())
if (combiningSpec == "hangul")
mCombiners.add(HangulCombiner())
} }
fun reset() { fun reset() {

View file

@ -16,6 +16,8 @@ class HangulCombiner : Combiner {
override fun processEvent(previousEvents: ArrayList<Event>?, event: Event): Event { override fun processEvent(previousEvents: ArrayList<Event>?, event: Event): Event {
if (event.mKeyCode == KeyCode.SHIFT) return event if (event.mKeyCode == KeyCode.SHIFT) return event
// previously we only used the combiner if codePoint > 0x1100 or codePoint == -1, but looks here it's not necessary
val event = HangulEventDecoder.decodeSoftwareKeyEvent(event)
if (Character.isWhitespace(event.mCodePoint)) { if (Character.isWhitespace(event.mCodePoint)) {
val text = combiningStateFeedback val text = combiningStateFeedback
reset() reset()

View file

@ -34,7 +34,6 @@ import helium314.keyboard.latin.utils.ToolbarKey
import helium314.keyboard.latin.utils.defaultPinnedToolbarPref import helium314.keyboard.latin.utils.defaultPinnedToolbarPref
import helium314.keyboard.latin.utils.getResourceSubtypes import helium314.keyboard.latin.utils.getResourceSubtypes
import helium314.keyboard.latin.utils.locale import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.mainLayoutName
import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty
import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.latin.utils.upgradeToolbarPrefs import helium314.keyboard.latin.utils.upgradeToolbarPrefs
@ -596,6 +595,18 @@ fun checkVersionUpgrade(context: Context) {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value as Float) e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value) e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value)
} else { } else {
if (key == Settings.PREF_ADDITIONAL_SUBTYPES || key == Settings.PREF_ENABLED_SUBTYPES) {
val subtypes = prefs.getString(key, "")!!.split(Separators.SETS).filter { it.isNotEmpty() }.map {
val st = it.toSettingsSubtype()
if (st.locale.language == "ko") st.with(ExtraValue.COMBINING_RULES, "hangul")
else st
}
e.putString(key, subtypes.joinToString(Separators.SETS) { it.toPref() })
} else if (key == Settings.PREF_SELECTED_SUBTYPE) {
val subtype = prefs.getString(key, "")!!.toSettingsSubtype()
if (subtype.locale.language == "ko")
e.putString(key, subtype.with(ExtraValue.COMBINING_RULES, "hangul").toPref())
}
return@forEach return@forEach
} }
e.remove(key) e.remove(key)

View file

@ -10,8 +10,6 @@ import androidx.annotation.NonNull;
import helium314.keyboard.event.CombinerChain; import helium314.keyboard.event.CombinerChain;
import helium314.keyboard.event.Event; import helium314.keyboard.event.Event;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode; import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo; import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData; import helium314.keyboard.latin.common.ComposedData;
@ -73,7 +71,7 @@ public final class WordComposer {
private boolean mIsOnlyFirstCharCapitalized; private boolean mIsOnlyFirstCharCapitalized;
public WordComposer() { public WordComposer() {
mCombinerChain = new CombinerChain(""); mCombinerChain = new CombinerChain("", "");
mEvents = new ArrayList<>(); mEvents = new ArrayList<>();
mAutoCorrection = null; mAutoCorrection = null;
mIsResumed = false; mIsResumed = false;
@ -81,11 +79,6 @@ public final class WordComposer {
mCursorPositionWithinWord = 0; mCursorPositionWithinWord = 0;
mRejectedBatchModeSuggestion = null; mRejectedBatchModeSuggestion = null;
refreshTypedWordCache(); refreshTypedWordCache();
final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
if (keyboard != null)
// initializing with the right state is important for the spell checker,
// which creates a new WordComposer when receiving suggestions
mCombinerChain.setHangul(keyboard.mId.mSubtype.getLocale().getLanguage().equals("ko"));
} }
public ComposedData getComposedDataSnapshot() { public ComposedData getComposedDataSnapshot() {
@ -99,14 +92,11 @@ public final class WordComposer {
public void restartCombining(final String combiningSpec) { public void restartCombining(final String combiningSpec) {
final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec; final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
if (!nonNullCombiningSpec.equals(mCombiningSpec)) { if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString()); mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString(), nonNullCombiningSpec);
mCombiningSpec = nonNullCombiningSpec; mCombiningSpec = nonNullCombiningSpec;
} }
} }
/** Forwards the state to CombinerChain, which disables or enables the Hangul combiner */
public void setHangul(final boolean enabled) { mCombinerChain.setHangul(enabled); }
/** /**
* Clear out the keys registered so far. * Clear out the keys registered so far.
*/ */
@ -218,11 +208,6 @@ public final class WordComposer {
// TODO: compute where that puts us inside the events // TODO: compute where that puts us inside the events
} }
public void resetInvalidCursorPosition() {
if (mCursorPositionWithinWord > mCodePointSize)
mCursorPositionWithinWord = 0;
}
public boolean isCursorFrontOrMiddleOfComposingWord() { public boolean isCursorFrontOrMiddleOfComposingWord() {
if (DebugFlags.DEBUG_ENABLED && mCursorPositionWithinWord > mCodePointSize) { if (DebugFlags.DEBUG_ENABLED && mCursorPositionWithinWord > mCodePointSize) {
throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord

View file

@ -77,7 +77,7 @@ public final class Constants {
/** Indicates that this subtype is an additional subtype that the user defined. This extra value is private to LatinIME. */ /** Indicates that this subtype is an additional subtype that the user defined. This extra value is private to LatinIME. */
public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype"; public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
/** The subtype extra value used to specify the combining rules (currently not used). */ /** The subtype extra value used to specify the combining rules. */
public static final String COMBINING_RULES = "CombiningRules"; public static final String COMBINING_RULES = "CombiningRules";
/** Overrides the general popup order setting */ /** Overrides the general popup order setting */

View file

@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import helium314.keyboard.event.Event; import helium314.keyboard.event.Event;
import helium314.keyboard.event.HangulEventDecoder;
import helium314.keyboard.event.InputTransaction; import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard; import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardSwitcher; import helium314.keyboard.keyboard.KeyboardSwitcher;
@ -130,7 +129,7 @@ public final class InputLogic {
* <p> * <p>
* Call this when input starts or restarts in some editor (typically, in onStartInputView). * Call this when input starts or restarts in some editor (typically, in onStartInputView).
* *
* @param combiningSpec the combining spec string for this subtype * @param combiningSpec the combining spec string for this subtype (from extra value)
* @param settingsValues the current settings values * @param settingsValues the current settings values
*/ */
public void startInput(final String combiningSpec, final SettingsValues settingsValues) { public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
@ -425,35 +424,7 @@ public final class InputLogic {
final String currentKeyboardScript, final LatinIME.UIHandler handler) { final String currentKeyboardScript, final LatinIME.UIHandler handler) {
mWordBeingCorrectedByCursor = null; mWordBeingCorrectedByCursor = null;
mJustRevertedACommit = false; mJustRevertedACommit = false;
final Event processedEvent; final Event processedEvent = mWordComposer.processEvent(event);
if (currentKeyboardScript.equals(ScriptUtils.SCRIPT_HANGUL)) {
// only use the Hangul chain if codepoint may actually be Hangul
// todo: this whole hangul-related logic should probably be somewhere else
// need to use hangul combiner for functional keys (codePoint -1), because otherwise the current word
// seems to get deleted / replaced by space during mConnection.endBatchEdit()
if (event.getMCodePoint() >= 0x1100 || event.getMCodePoint() == -1) {
mWordComposer.setHangul(true);
final Event hangulDecodedEvent = HangulEventDecoder.decodeSoftwareKeyEvent(event);
// todo: here hangul combiner does already consume the event, and appends typed codepoint
// to the current word instead of considering the cursor position
// position is actually not visible to the combiner, how to fix?
processedEvent = mWordComposer.processEvent(hangulDecodedEvent);
if (event.getMKeyCode() == KeyCode.DELETE)
mWordComposer.resetInvalidCursorPosition();
} else {
mWordComposer.setHangul(false);
final boolean wasComposingWord = mWordComposer.isComposingWord();
processedEvent = mWordComposer.processEvent(event);
// workaround for space and some other separators deleting / replacing the word
if (wasComposingWord && !mWordComposer.isComposingWord()) {
mWordComposer.resetInvalidCursorPosition();
mConnection.finishComposingText();
}
}
} else {
mWordComposer.setHangul(false);
processedEvent = mWordComposer.processEvent(event);
}
final InputTransaction inputTransaction = new InputTransaction(settingsValues, final InputTransaction inputTransaction = new InputTransaction(settingsValues,
processedEvent, SystemClock.uptimeMillis(), mSpaceState, processedEvent, SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode)); getActualCapsMode(settingsValues, keyboardShiftMode));

View file

@ -335,6 +335,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
} }
final WordComposer composer = new WordComposer(); final WordComposer composer = new WordComposer();
if (mLocale.getLanguage().equals("ko"))
composer.restartCombining("hangul");
final int[] codePoints = StringUtils.toCodePointArray(text); final int[] codePoints = StringUtils.toCodePointArray(text);
final int[] coordinates; final int[] coordinates;
coordinates = keyboard.getCoordinates(codePoints); coordinates = keyboard.getCoordinates(codePoints);

View file

@ -763,7 +763,7 @@
android:imeSubtypeLocale="ko" android:imeSubtypeLocale="ko"
android:languageTag="ko" android:languageTag="ko"
android:imeSubtypeMode="keyboard" android:imeSubtypeMode="keyboard"
android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean,SupportTouchPositionCorrection,EmojiCapable" android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean,CombiningRules=hangul,SupportTouchPositionCorrection,EmojiCapable"
android:isAsciiCapable="false" android:isAsciiCapable="false"
/> />
<subtype android:icon="@drawable/ic_ime_switcher" <subtype android:icon="@drawable/ic_ime_switcher"
@ -772,7 +772,7 @@
android:imeSubtypeLocale="ko" android:imeSubtypeLocale="ko"
android:languageTag="ko" android:languageTag="ko"
android:imeSubtypeMode="keyboard" android:imeSubtypeMode="keyboard"
android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean_phonetic,SupportTouchPositionCorrection,EmojiCapable" android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean_phonetic,CombiningRules=hangul,SupportTouchPositionCorrection,EmojiCapable"
android:isAsciiCapable="false" android:isAsciiCapable="false"
/> />
<subtype android:icon="@drawable/ic_ime_switcher" <subtype android:icon="@drawable/ic_ime_switcher"
@ -781,7 +781,7 @@
android:imeSubtypeLocale="ko" android:imeSubtypeLocale="ko"
android:languageTag="ko" android:languageTag="ko"
android:imeSubtypeMode="keyboard" android:imeSubtypeMode="keyboard"
android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean_sebeolsik_390,SupportTouchPositionCorrection,EmojiCapable" android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean_sebeolsik_390,CombiningRules=hangul,SupportTouchPositionCorrection,EmojiCapable"
android:isAsciiCapable="false" android:isAsciiCapable="false"
/> />
<subtype android:icon="@drawable/ic_ime_switcher" <subtype android:icon="@drawable/ic_ime_switcher"
@ -790,7 +790,7 @@
android:imeSubtypeLocale="ko" android:imeSubtypeLocale="ko"
android:languageTag="ko" android:languageTag="ko"
android:imeSubtypeMode="keyboard" android:imeSubtypeMode="keyboard"
android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean_sebeolsik_final,SupportTouchPositionCorrection,EmojiCapable" android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:korean_sebeolsik_final,CombiningRules=hangul,SupportTouchPositionCorrection,EmojiCapable"
android:isAsciiCapable="false" android:isAsciiCapable="false"
/> />
<subtype android:icon="@drawable/ic_ime_switcher" <subtype android:icon="@drawable/ic_ime_switcher"

View file

@ -18,11 +18,13 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.ShadowFacilitator2.Companion.lastAddedWord import helium314.keyboard.latin.ShadowFacilitator2.Companion.lastAddedWord
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.inputlogic.InputLogic import helium314.keyboard.latin.inputlogic.InputLogic
import helium314.keyboard.latin.inputlogic.SpaceState import helium314.keyboard.latin.inputlogic.SpaceState
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ScriptUtils import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.getTimestamp import helium314.keyboard.latin.utils.getTimestamp
import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.prefs
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -141,7 +143,7 @@ class InputLogicTest {
@Test fun insertLetterIntoWordHangul() { @Test fun insertLetterIntoWordHangul() {
if (BuildConfig.BUILD_TYPE == "runTests") return if (BuildConfig.BUILD_TYPE == "runTests") return
reset() reset()
currentScript = ScriptUtils.SCRIPT_HANGUL latinIME.switchToSubtype(SubtypeSettings.getResourceSubtypesForLocale("ko".constructLocale()).first())
chainInput("ㅛㅎㄹㅎㅕㅛ") chainInput("ㅛㅎㄹㅎㅕㅛ")
setCursorPosition(3) setCursorPosition(3)
input('ㄲ') // fails, as expected from the hangul issue when processing the event in onCodeInput input('ㄲ') // fails, as expected from the hangul issue when processing the event in onCodeInput
@ -155,7 +157,7 @@ class InputLogicTest {
// see issue 1447 // see issue 1447
@Test fun separatorAfterHangul() { @Test fun separatorAfterHangul() {
reset() reset()
currentScript = ScriptUtils.SCRIPT_HANGUL latinIME.switchToSubtype(SubtypeSettings.getResourceSubtypesForLocale("ko".constructLocale()).first())
chainInput("ㅛ.") chainInput("ㅛ.")
assertEquals("ㅛ.", text) assertEquals("ㅛ.", text)
} }
@ -163,7 +165,7 @@ class InputLogicTest {
// see issue 1551 (debug only) // see issue 1551 (debug only)
@Test fun deleteHangul() { @Test fun deleteHangul() {
reset() reset()
currentScript = ScriptUtils.SCRIPT_HANGUL latinIME.switchToSubtype(SubtypeSettings.getResourceSubtypesForLocale("ko".constructLocale()).first())
setText("ㅛㅛ ") setText("ㅛㅛ ")
functionalKeyPress(KeyCode.DELETE) functionalKeyPress(KeyCode.DELETE)
functionalKeyPress(KeyCode.DELETE) functionalKeyPress(KeyCode.DELETE)