diff --git a/java/res/drawable/empty.xml b/java/res/drawable/empty.xml new file mode 100644 index 000000000..2bce7ee46 --- /dev/null +++ b/java/res/drawable/empty.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index e7b51e048..d1b6216ce 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -573,4 +573,43 @@ Tip: You can download and remove dictionaries by going to <b>Languages & This resource is copied from packages/apps/Settings/res/values/strings.xml --> \u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ + + Arabic + Armenian (Phonetic) + AZERTY + Bengali + Bengali (Akkhor) + BÉPO + Bulgarian + Bulgarian (BDS) + Colemak + Dvorak + East Slavic + Farsi + Georgian + Greek + Hebrew + Hindi + Hindi (Compact) + Kannada + Khmer + Lao + Malayalam + Marathi + Mongolian + Nepali (Romanized) + Nepali (Traditional) + Nordic + PC QWERTY + QWERTY + QWERTZ + Serbian (QWERTZ) + Sinhala + South Slavic + Spanish + Swiss + Tamil + Telugu + Thai + Uzbek diff --git a/java/res/xml/kbd_bepo.xml b/java/res/xml/kbd_bepo.xml new file mode 100644 index 000000000..d0226991e --- /dev/null +++ b/java/res/xml/kbd_bepo.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/java/res/xml/keyboard_layout_set_bepo.xml b/java/res/xml/keyboard_layout_set_bepo.xml new file mode 100644 index 000000000..d9d248d29 --- /dev/null +++ b/java/res/xml/keyboard_layout_set_bepo.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml index 1f02f3b40..c97b77ac4 100644 --- a/java/res/xml/method.xml +++ b/java/res/xml/method.xml @@ -1,812 +1,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:settingsActivity="org.futo.inputmethod.latin.uix.settings.SettingsActivity" + android:isDefault="true" + android:supportsSwitchingToNextInputMethod="true" + android:supportsInlineSuggestions="true"> + diff --git a/java/res/xml/rowkeys_bepo1.xml b/java/res/xml/rowkeys_bepo1.xml new file mode 100644 index 000000000..9d4decbfa --- /dev/null +++ b/java/res/xml/rowkeys_bepo1.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + diff --git a/java/res/xml/rowkeys_bepo2.xml b/java/res/xml/rowkeys_bepo2.xml new file mode 100644 index 000000000..360a0c8b1 --- /dev/null +++ b/java/res/xml/rowkeys_bepo2.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + diff --git a/java/res/xml/rowkeys_bepo3.xml b/java/res/xml/rowkeys_bepo3.xml new file mode 100644 index 000000000..a449c62e0 --- /dev/null +++ b/java/res/xml/rowkeys_bepo3.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/java/res/xml/rows_bepo.xml b/java/res/xml/rows_bepo.xml new file mode 100644 index 000000000..6e97fea37 --- /dev/null +++ b/java/res/xml/rows_bepo.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/org/futo/inputmethod/keyboard/KeyboardSwitcher.java index 148fe03bb..0d7f7fbd3 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardSwitcher.java @@ -28,7 +28,6 @@ import androidx.annotation.NonNull; import org.futo.inputmethod.event.Event; import org.futo.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; -import org.futo.inputmethod.keyboard.emoji.EmojiPalettesView; import org.futo.inputmethod.keyboard.internal.KeyboardState; import org.futo.inputmethod.keyboard.internal.KeyboardTextsSet; import org.futo.inputmethod.latin.InputView; @@ -36,6 +35,7 @@ import org.futo.inputmethod.latin.LatinIME; import org.futo.inputmethod.latin.LatinIMELegacy; import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.RichInputMethodManager; +import org.futo.inputmethod.latin.Subtypes; import org.futo.inputmethod.latin.WordComposer; import org.futo.inputmethod.latin.define.ProductionFlags; import org.futo.inputmethod.latin.settings.Settings; @@ -181,8 +181,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { || !newKeyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype); final int languageOnSpacebarFormatType = LanguageOnSpacebarUtils .getLanguageOnSpacebarFormatType(newKeyboard.mId.mSubtype); - final boolean hasMultipleEnabledIMEsOrSubtypes = mRichImm - .hasMultipleEnabledIMEsOrSubtypes(true /* shouldIncludeAuxiliarySubtypes */); + final boolean hasMultipleEnabledIMEsOrSubtypes = Subtypes.INSTANCE.hasMultipleEnabledSubtypes(mThemeContext); keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType, hasMultipleEnabledIMEsOrSubtypes); } diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index 76ca689cd..183b136ce 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -63,6 +63,7 @@ import org.futo.inputmethod.latin.uix.deferGetSetting import org.futo.inputmethod.latin.uix.deferSetSetting import org.futo.inputmethod.latin.uix.differsFrom import org.futo.inputmethod.latin.uix.getSetting +import org.futo.inputmethod.latin.uix.getSettingBlocking import org.futo.inputmethod.latin.uix.getSettingFlow import org.futo.inputmethod.latin.uix.setSetting import org.futo.inputmethod.latin.uix.theme.DarkColorScheme @@ -275,7 +276,21 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } lifecycleScope.launch { - saveSubtypes() + val onNewSubtype: (String) -> Unit = { + val activeSubtype = it.ifEmpty { + getSettingBlocking(SubtypesSetting).firstOrNull() + } + + if(activeSubtype != null) { + changeInputMethodSubtype(Subtypes.convertToSubtype(activeSubtype)) + } + } + + onNewSubtype(getSetting(ActiveSubtype)) + + dataStore.data.collect { + onNewSubtype(it[ActiveSubtype.key] ?: ActiveSubtype.default) + } } } @@ -411,8 +426,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save languageModelFacilitator.saveHistoryLog() } - override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) { - super.onCurrentInputMethodSubtypeChanged(newSubtype) + private fun changeInputMethodSubtype(newSubtype: InputMethodSubtype?) { latinIMELegacy.onCurrentInputMethodSubtypeChanged(newSubtype) } diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 01384e574..8b3cfbc41 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -32,7 +32,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Color; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.os.Build; @@ -61,8 +60,6 @@ import androidx.core.content.ContextCompat; import org.futo.inputmethod.accessibility.AccessibilityUtils; import org.futo.inputmethod.annotations.UsedForTesting; -import org.futo.inputmethod.compat.BuildCompatUtils; -import org.futo.inputmethod.compat.EditorInfoCompatUtils; import org.futo.inputmethod.compat.ViewOutlineProviderCompatUtils; import org.futo.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater; import org.futo.inputmethod.dictionarypack.DictionaryPackConstants; @@ -90,7 +87,7 @@ import org.futo.inputmethod.latin.settings.SettingsValues; import org.futo.inputmethod.latin.suggestions.SuggestionStripView; import org.futo.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import org.futo.inputmethod.latin.touchinputconsumer.GestureConsumer; -import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner; +import org.futo.inputmethod.latin.uix.actions.SwitchLanguageActionKt; import org.futo.inputmethod.latin.uix.settings.SettingsActivity; import org.futo.inputmethod.latin.utils.ApplicationUtils; import org.futo.inputmethod.latin.utils.DialogUtils; @@ -176,7 +173,7 @@ public class LatinIMELegacy implements KeyboardActionListener, private final SuggestionStripController mSuggestionStripController; private RichInputMethodManager mRichImm; - @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; + public final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector; private StatsUtilsManager mStatsUtilsManager; @@ -329,9 +326,6 @@ public class LatinIMELegacy implements KeyboardActionListener, case MSG_DEALLOCATE_MEMORY: latinImeLegacy.deallocateMemory(); break; - case MSG_SWITCH_LANGUAGE_AUTOMATICALLY: - latinImeLegacy.switchLanguage((InputMethodSubtype)msg.obj); - break; } } @@ -465,10 +459,6 @@ public class LatinIMELegacy implements KeyboardActionListener, obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } - public void postSwitchLanguage(final InputMethodSubtype subtype) { - obtainMessage(MSG_SWITCH_LANGUAGE_AUTOMATICALLY, subtype).sendToTarget(); - } - // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; @@ -589,23 +579,6 @@ public class LatinIMELegacy implements KeyboardActionListener, mCurrentSubtypeHasBeenUsed = true; } - public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { - final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() - .getCurrentInputMethodSubtype(); - final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; - final boolean currentSubtypeHasBeenUsed = mCurrentSubtypeHasBeenUsed; - if (currentSubtypeHasBeenUsed) { - mLastActiveSubtype = currentSubtype; - mCurrentSubtypeHasBeenUsed = false; - } - if (currentSubtypeHasBeenUsed - && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) - && !currentSubtype.equals(lastActiveSubtype)) { - richImm.setInputMethodAndSubtype(token, lastActiveSubtype); - return; - } - richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); - } } // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial @@ -929,18 +902,7 @@ public class LatinIMELegacy implements KeyboardActionListener, } void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { - // If the primary hint language does not match the current subtype language, then try - // to switch to the primary hint language. - // TODO: Support all the locales in EditorInfo#hintLocales. - final Locale primaryHintLocale = EditorInfoCompatUtils.getPrimaryHintLocale(editorInfo); - if (primaryHintLocale == null) { - return; - } - final InputMethodSubtype newSubtype = mRichImm.findSubtypeByLocale(primaryHintLocale); - if (newSubtype == null || newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype())) { - return; - } - mHandler.postSwitchLanguage(newSubtype); + } public void updateMainKeyboardViewSettings() { @@ -1374,11 +1336,8 @@ public class LatinIMELegacy implements KeyboardActionListener, if (isShowingOptionDialog()) return false; switch (requestCode) { case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER: - if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { - mRichImm.getInputMethodManager().showInputMethodPicker(); - return true; - } - return false; + ((LatinIME)mInputMethodService).getUixManager().showLanguageSwitcher(); + return true; } return false; } @@ -1459,13 +1418,8 @@ public class LatinIMELegacy implements KeyboardActionListener, return mOptionsDialog != null && mOptionsDialog.isShowing(); } - public void switchLanguage(final InputMethodSubtype subtype) { - final IBinder token = mInputMethodService.getWindow().getWindow().getAttributes().token; - mRichImm.setInputMethodAndSubtype(token, subtype); - } - public void switchToNextSubtype() { - mRichImm.getInputMethodManager().showInputMethodPicker(); + SwitchLanguageActionKt.switchToNextLanguage(mInputMethodService); } // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for @@ -2006,12 +1960,6 @@ public class LatinIMELegacy implements KeyboardActionListener, mDictionaryFacilitator.clearUserHistoryDictionary(mInputMethodService); } - @UsedForTesting - List getEnabledSubtypesForTest() { - return (mRichImm != null) ? mRichImm.getMyEnabledInputMethodSubtypeList( - true /* allowsImplicitlySelectedSubtypes */) : new ArrayList(); - } - public void dumpDictionaryForDebug(final String dictName) { if (!mDictionaryFacilitator.isActive()) { resetDictionaryFacilitatorIfNecessary(); diff --git a/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java b/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java index 4a08ff1de..513b2eb9a 100644 --- a/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java @@ -102,9 +102,6 @@ public class RichInputMethodManager { // Initialize additional subtypes. SubtypeLocaleUtils.init(context); - final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(); - mImmWrapper.mImm.setAdditionalInputMethodSubtypes( - getInputMethodIdOfThisIme(), additionalSubtypes); // Initialize the current input method subtype and the shortcut IME. refreshSubtypeCaches(); @@ -122,110 +119,6 @@ public class RichInputMethodManager { return mImmWrapper.mImm; } - public List getMyEnabledInputMethodSubtypeList( - boolean allowsImplicitlySelectedSubtypes) { - return getEnabledInputMethodSubtypeList( - getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes); - } - - public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) { - if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) { - return true; - } - // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)} - // because the current device is running ICS or previous and lacks the API. - if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) { - return true; - } - return switchToNextInputMethodAndSubtype(token); - } - - private boolean switchToNextInputSubtypeInThisIme(final IBinder token, - final boolean onlyCurrentIme) { - final InputMethodManager imm = mImmWrapper.mImm; - final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype(); - final List enabledSubtypes = getMyEnabledInputMethodSubtypeList( - true /* allowsImplicitlySelectedSubtypes */); - final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes); - if (currentIndex == INDEX_NOT_FOUND) { - Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" - + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype)); - return false; - } - final int nextIndex = (currentIndex + 1) % enabledSubtypes.size(); - if (nextIndex <= currentIndex && !onlyCurrentIme) { - // The current subtype is the last or only enabled one and it needs to switch to - // next IME. - return false; - } - final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex); - setInputMethodAndSubtype(token, nextSubtype); - return true; - } - - private boolean switchToNextInputMethodAndSubtype(final IBinder token) { - final InputMethodManager imm = mImmWrapper.mImm; - final List enabledImis = imm.getEnabledInputMethodList(); - final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis); - if (currentIndex == INDEX_NOT_FOUND) { - Log.w(TAG, "Can't find current IME in enabled IMEs: IME package=" - + getInputMethodInfoOfThisIme().getPackageName()); - return false; - } - final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis); - final List enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi, - true /* allowsImplicitlySelectedSubtypes */); - if (enabledSubtypes.isEmpty()) { - // The next IME has no subtype. - imm.setInputMethod(token, nextImi.getId()); - return true; - } - final InputMethodSubtype firstSubtype = enabledSubtypes.get(0); - imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype); - return true; - } - - private static int getImiIndexInList(final InputMethodInfo inputMethodInfo, - final List imiList) { - final int count = imiList.size(); - for (int index = 0; index < count; index++) { - final InputMethodInfo imi = imiList.get(index); - if (imi.equals(inputMethodInfo)) { - return index; - } - } - return INDEX_NOT_FOUND; - } - - // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}. - private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex, - final List imiList) { - final int count = imiList.size(); - for (int i = 1; i < count; i++) { - final int nextIndex = (currentIndex + i) % count; - final InputMethodInfo nextImi = imiList.get(nextIndex); - if (!isAuxiliaryIme(nextImi)) { - return nextImi; - } - } - return imiList.get(currentIndex); - } - - // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined. - private static boolean isAuxiliaryIme(final InputMethodInfo imi) { - final int count = imi.getSubtypeCount(); - if (count == 0) { - return false; - } - for (int index = 0; index < count; index++) { - final InputMethodSubtype subtype = imi.getSubtypeAt(index); - if (!subtype.isAuxiliary()) { - return false; - } - } - return true; - } - private static class InputMethodInfoCache { private final InputMethodManager mImm; private final String mImePackageName; @@ -287,38 +180,6 @@ public class RichInputMethodManager { return getInputMethodInfoOfThisIme().getId(); } - public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { - return checkIfSubtypeBelongsToList(subtype, - getEnabledInputMethodSubtypeList( - getInputMethodInfoOfThisIme(), - true /* allowsImplicitlySelectedSubtypes */)); - } - - public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( - final InputMethodSubtype subtype) { - final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype); - final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(subtype, - getMyEnabledInputMethodSubtypeList(false /* allowsImplicitlySelectedSubtypes */)); - return subtypeEnabled && !subtypeExplicitlyEnabled; - } - - private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype, - final List subtypes) { - return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND; - } - - private static int getSubtypeIndexInList(final InputMethodSubtype subtype, - final List subtypes) { - final int count = subtypes.size(); - for (int index = 0; index < count; index++) { - final InputMethodSubtype ims = subtypes.get(index); - if (ims.equals(subtype)) { - return index; - } - } - return INDEX_NOT_FOUND; - } - public void onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) { updateCurrentSubtype(newSubtype); updateShortcutIme(); @@ -355,142 +216,9 @@ public class RichInputMethodManager { return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype()); } - public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { - final List enabledImis = mImmWrapper.mImm.getEnabledInputMethodList(); - return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis); - } - - public boolean hasMultipleEnabledSubtypesInThisIme( - final boolean shouldIncludeAuxiliarySubtypes) { - final List imiList = Collections.singletonList( - getInputMethodInfoOfThisIme()); - return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList); - } - - private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes, - final List imiList) { - // Number of the filtered IMEs - int filteredImisCount = 0; - - for (InputMethodInfo imi : imiList) { - // We can return true immediately after we find two or more filtered IMEs. - if (filteredImisCount > 1) return true; - final List subtypes = getEnabledInputMethodSubtypeList(imi, true); - // IMEs that have no subtypes should be counted. - if (subtypes.isEmpty()) { - ++filteredImisCount; - continue; - } - - int auxCount = 0; - for (InputMethodSubtype subtype : subtypes) { - if (subtype.isAuxiliary()) { - ++auxCount; - } - } - final int nonAuxCount = subtypes.size() - auxCount; - - // IMEs that have one or more non-auxiliary subtypes should be counted. - // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary - // subtypes should be counted as well. - if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { - ++filteredImisCount; - } - } - - if (filteredImisCount > 1) { - return true; - } - final List subtypes = getMyEnabledInputMethodSubtypeList(true); - int keyboardCount = 0; - // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's - // both explicitly and implicitly enabled input method subtype. - // (The current IME should be LatinIME.) - for (InputMethodSubtype subtype : subtypes) { - if (KEYBOARD_MODE.equals(subtype.getMode())) { - ++keyboardCount; - } - } - return keyboardCount > 1; - } - - public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString, - final String keyboardLayoutSetName) { - final InputMethodInfo myImi = getInputMethodInfoOfThisIme(); - final int count = myImi.getSubtypeCount(); - for (int i = 0; i < count; i++) { - final InputMethodSubtype subtype = myImi.getSubtypeAt(i); - final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); - if (localeString.equals(subtype.getLocale()) - && keyboardLayoutSetName.equals(layoutName)) { - return subtype; - } - } - return null; - } - - public InputMethodSubtype findSubtypeByLocale(final Locale locale) { - // Find the best subtype based on a straightforward matching algorithm. - // TODO: Use LocaleList#getFirstMatch() instead. - final List subtypes = - getMyEnabledInputMethodSubtypeList(true /* allowsImplicitlySelectedSubtypes */); - final int count = subtypes.size(); - for (int i = 0; i < count; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); - if (subtypeLocale.equals(locale)) { - return subtype; - } - } - for (int i = 0; i < count; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); - if (subtypeLocale.getLanguage().equals(locale.getLanguage()) && - subtypeLocale.getCountry().equals(locale.getCountry()) && - subtypeLocale.getVariant().equals(locale.getVariant())) { - return subtype; - } - } - for (int i = 0; i < count; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); - if (subtypeLocale.getLanguage().equals(locale.getLanguage()) && - subtypeLocale.getCountry().equals(locale.getCountry())) { - return subtype; - } - } - for (int i = 0; i < count; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); - if (subtypeLocale.getLanguage().equals(locale.getLanguage())) { - return subtype; - } - } - return null; - } - - public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) { - mImmWrapper.mImm.setInputMethodAndSubtype( - token, getInputMethodIdOfThisIme(), subtype); - } - - public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) { - mImmWrapper.mImm.setAdditionalInputMethodSubtypes( - getInputMethodIdOfThisIme(), subtypes); - // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of - // subtypes again next time. - refreshSubtypeCaches(); - } - - private List getEnabledInputMethodSubtypeList(final InputMethodInfo imi, - final boolean allowsImplicitlySelectedSubtypes) { - return mInputMethodInfoCache.getEnabledInputMethodSubtypeList( - imi, allowsImplicitlySelectedSubtypes); - } - public void refreshSubtypeCaches() { mInputMethodInfoCache.clear(); - updateCurrentSubtype(mImmWrapper.mImm.getCurrentInputMethodSubtype()); + updateCurrentSubtype(Subtypes.INSTANCE.getActiveSubtype(mContext)); updateShortcutIme(); } @@ -545,13 +273,7 @@ public class RichInputMethodManager { mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); } final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype; - final boolean implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( - richSubtype.getRawSubtype()); final Locale systemLocale = mContext.getResources().getConfiguration().locale; - LanguageOnSpacebarUtils.onSubtypeChanged( - richSubtype, implicitlyEnabledSubtype, systemLocale); - LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList( - true /* allowsImplicitlySelectedSubtypes */)); // TODO: Update an icon for shortcut IME final Map> shortcuts = diff --git a/java/src/org/futo/inputmethod/latin/RichInputMethodSubtype.java b/java/src/org/futo/inputmethod/latin/RichInputMethodSubtype.java index 45d0d8965..77c09eae0 100644 --- a/java/src/org/futo/inputmethod/latin/RichInputMethodSubtype.java +++ b/java/src/org/futo/inputmethod/latin/RichInputMethodSubtype.java @@ -209,14 +209,7 @@ public class RichInputMethodSubtype { @Nonnull public static RichInputMethodSubtype getNoLanguageSubtype() { RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype; - if (noLanguageSubtype == null) { - final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance() - .findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY); - if (rawNoLanguageSubtype != null) { - noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype); - } - } + if (noLanguageSubtype != null) { sNoLanguageSubtype = noLanguageSubtype; return noLanguageSubtype; @@ -230,14 +223,7 @@ public class RichInputMethodSubtype { @Nonnull public static RichInputMethodSubtype getEmojiSubtype() { RichInputMethodSubtype emojiSubtype = sEmojiSubtype; - if (emojiSubtype == null) { - final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance() - .findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); - if (rawEmojiSubtype != null) { - emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype); - } - } + if (emojiSubtype != null) { sEmojiSubtype = emojiSubtype; return emojiSubtype; diff --git a/java/src/org/futo/inputmethod/latin/Subtypes.kt b/java/src/org/futo/inputmethod/latin/Subtypes.kt index 926561374..9ea2120f7 100644 --- a/java/src/org/futo/inputmethod/latin/Subtypes.kt +++ b/java/src/org/futo/inputmethod/latin/Subtypes.kt @@ -1,26 +1,256 @@ package org.futo.inputmethod.latin import android.content.Context +import android.content.Intent import android.view.inputmethod.InputMethodManager +import android.view.inputmethod.InputMethodSubtype +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey +import okhttp3.internal.toImmutableList +import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.uix.SettingsKey -import org.futo.inputmethod.latin.uix.setSetting +import org.futo.inputmethod.latin.uix.getSettingBlocking +import org.futo.inputmethod.latin.uix.setSettingBlocking +import org.futo.inputmethod.latin.uix.settings.NavigationItem +import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle +import org.futo.inputmethod.latin.uix.settings.SettingsActivity +import org.futo.inputmethod.latin.uix.settings.useDataStoreValueBlocking +import org.futo.inputmethod.latin.uix.theme.Typography +import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils +import java.util.Locale val SubtypesSetting = SettingsKey( stringSetPreferencesKey("subtypes"), setOf() ) -suspend fun Context.saveSubtypes() { - val inputMethodManager = getSystemService(android.inputmethodservice.InputMethodService.INPUT_METHOD_SERVICE) as InputMethodManager - val inputMethodList = inputMethodManager.getEnabledInputMethodSubtypeList( - RichInputMethodManager.getInstance().inputMethodInfoOfThisIme, - true - ) +val ActiveSubtype = SettingsKey( + stringPreferencesKey("activeSubtype"), + "" +) - val encodedSubtypes = inputMethodList.map { - it.locale + ":" + (it.extraValue ?: "") + ":" + it.languageTag - }.toSet() +object Subtypes { + fun convertToSubtype(string: String): InputMethodSubtype { + val splits = string.split(":") + val locale = splits[0] - setSetting(SubtypesSetting, encodedSubtypes) + val extraValue = splits.getOrNull(1) ?: "" + val languageTag = splits.getOrNull(2) ?: "" + + return InputMethodSubtypeBuilder() + .setSubtypeLocale(locale) + .setSubtypeExtraValue(extraValue) + .setLanguageTag(languageTag) + .build() + } + + fun getActiveSubtype(context: Context): InputMethodSubtype { + val activeSubtype = context.getSettingBlocking(ActiveSubtype).ifEmpty { + context.getSettingBlocking(SubtypesSetting).firstOrNull() ?: "en_US:" + } + + return convertToSubtype(activeSubtype) + } + + fun hasMultipleEnabledSubtypes(context: Context): Boolean { + return context.getSettingBlocking(SubtypesSetting).size > 1 + } + + fun subtypeToString(subtype: InputMethodSubtype): String { + return subtype.locale + ":" + (subtype.extraValue ?: "") + ":" + subtype.languageTag + } + + fun removeLanguage(context: Context, entry: InputMethodSubtype) { + val value = subtypeToString(entry) + val currentSetting = context.getSettingBlocking(SubtypesSetting) + + context.setSettingBlocking(SubtypesSetting.key, currentSetting.filter { it != value && it != value.replace("::", ":") }.toSet()) + + if(context.getSettingBlocking(ActiveSubtype) == value) { + context.setSettingBlocking(ActiveSubtype.key, currentSetting.find { + it != value + } ?: "") + } + } + + fun addLanguage(context: Context, language: Locale, layout: String) { + val value = subtypeToString( + InputMethodSubtypeBuilder() + .setSubtypeLocale(language.toString()) + .setSubtypeExtraValue("KeyboardLayoutSet=$layout") + .build() + ) + val currentSetting = context.getSettingBlocking(SubtypesSetting) + + context.setSettingBlocking(SubtypesSetting.key, currentSetting + setOf(value)) + } + + fun getName(inputMethodSubtype: InputMethodSubtype): String { + return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(inputMethodSubtype) + } + + fun getNameForLocale(locale: String): String { + return getName(InputMethodSubtypeBuilder().setSubtypeLocale(locale).build()) + } + + fun getLocale(locale: String): Locale { + return Locale.forLanguageTag(locale.replace("_", "-")) + } + + fun getLocale(inputMethodSubtype: InputMethodSubtype): Locale { + return getLocale(inputMethodSubtype.locale) + } + + fun getLayoutName(context: Context, layout: String): String { + val resourceId = context.resources.getIdentifier("layout_$layout", "string", context.packageName) + if(resourceId == 0){ + return layout + } else { + return context.getString(resourceId) + } + } + + fun layoutsMappedByLanguage(layouts: Set): Map> { + val subtypes = layouts.map { + convertToSubtype(it) + } + + return HashMap>().apply { + subtypes.forEach { + val list = this.getOrPut(it.locale) { mutableListOf() } + list.add(it) + } + }.mapValues { it.value.toImmutableList() } + } +} + + +@Composable +@Preview +fun LanguageSwitcherDialog( + onDismiss: () -> Unit = { } +) { + val inspection = LocalInspectionMode.current + val context = LocalContext.current + val subtypeSet = if(inspection) { + setOf("en_US:", "pt_PT:", "lt:", "fr:KeyboardLayoutSet=bepo:") + } else { + useDataStoreValueBlocking(SubtypesSetting) + } + + val subtypes = remember(subtypeSet) { + Subtypes.layoutsMappedByLanguage(subtypeSet) + } + + val keys = remember(subtypes) { subtypes.keys.toList().sorted() } + + val activeSubtype = if(inspection) { + "pt_PT:" + } else { + useDataStoreValueBlocking(ActiveSubtype) + } + + Surface(shape = RoundedCornerShape(48.dp), color = MaterialTheme.colorScheme.background) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + Text( + "Select language", + textAlign = TextAlign.Center, + style = Typography.titleLarge, + modifier = Modifier + .fillMaxWidth() + .padding(0.dp, 16.dp) + ) + + LazyColumn(modifier = Modifier.weight(1.0f)) { + items(keys) { locale -> + + subtypes[locale]!!.forEach { subtype -> + val layout = Subtypes.getLayoutName(context, + subtype.getExtraValueOf(Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET) ?: "" + ) + + val title = if(inspection) { subtype.locale } else { Subtypes.getName(subtype) } + + val selected = activeSubtype == Subtypes.subtypeToString(subtype) + + val item = @Composable { + NavigationItem( + title = title, + subtitle = layout.ifBlank { null }, + style = NavigationItemStyle.MiscNoArrow, + navigate = { + context.setSettingBlocking(ActiveSubtype.key, Subtypes.subtypeToString(subtype)) + onDismiss() + } + ) + } + + if (selected) { + Surface(color = MaterialTheme.colorScheme.primary) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimary) { + item() + } + } + } else { + item() + } + + } + } + } + + Row(modifier = Modifier.height(64.dp)) { + Spacer(modifier = Modifier.weight(1.0f)) + TextButton(onClick = { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showInputMethodPicker() + + onDismiss() + }) { + Text("Switch Keyboard") + } + TextButton(onClick = { + val intent = Intent() + intent.setClass(context, SettingsActivity::class.java) + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + ) + intent.putExtra("navDest", "languages") + context.startActivity(intent) + + onDismiss() + }) { + Text("Languages") + } + Spacer(modifier = Modifier.width(32.dp)) + } + } + } } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java index da4bfbc8a..cb6436c0e 100644 --- a/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/org/futo/inputmethod/latin/SystemBroadcastReceiver.java @@ -59,12 +59,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final String intentAction = intent.getAction(); if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intentAction)) { Log.i(TAG, "Package has been replaced: " + context.getPackageName()); - // Need to restore additional subtypes because system always clears additional - // subtypes when the package is replaced. - RichInputMethodManager.init(context); - final RichInputMethodManager richImm = RichInputMethodManager.getInstance(); - final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); - richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); } else if (Intent.ACTION_LOCALE_CHANGED.equals(intentAction)) { diff --git a/java/src/org/futo/inputmethod/latin/uix/ImportResourceActivity.kt b/java/src/org/futo/inputmethod/latin/uix/ImportResourceActivity.kt index 4f237336f..83ee61047 100644 --- a/java/src/org/futo/inputmethod/latin/uix/ImportResourceActivity.kt +++ b/java/src/org/futo/inputmethod/latin/uix/ImportResourceActivity.kt @@ -38,7 +38,8 @@ import org.futo.inputmethod.latin.Dictionary import org.futo.inputmethod.latin.LatinIMELegacy import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.ReadOnlyBinaryDictionary -import org.futo.inputmethod.latin.RichInputMethodManager +import org.futo.inputmethod.latin.Subtypes +import org.futo.inputmethod.latin.SubtypesSetting import org.futo.inputmethod.latin.uix.settings.NavigationItem import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle import org.futo.inputmethod.latin.uix.settings.ScreenTitle @@ -68,13 +69,16 @@ data class InputLanguage( ) fun getActiveLanguages(context: Context): List { - RichInputMethodManager.init(context) - - return RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypeList(true).map { - val name = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(it) - - InputLanguage(it.locale, name, it) - }.toList() + SubtypeLocaleUtils.init(context) + return context.getSettingBlocking(SubtypesSetting) + .let { Subtypes.layoutsMappedByLanguage(it) } + .map { + InputLanguage( + it.value.first().locale, + Subtypes.getName(it.value.first()), + it.value.first() + ) + } } diff --git a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt index 0933672c9..7bd827110 100644 --- a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt +++ b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.futo.inputmethod.latin.AudioAndHapticFeedbackManager import org.futo.inputmethod.latin.BuildConfig +import org.futo.inputmethod.latin.LanguageSwitcherDialog import org.futo.inputmethod.latin.LatinIME import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.SuggestedWords @@ -57,6 +58,7 @@ import org.futo.inputmethod.latin.uix.actions.ActionRegistry import org.futo.inputmethod.latin.uix.actions.EmojiAction import org.futo.inputmethod.latin.uix.settings.SettingsActivity import org.futo.inputmethod.latin.uix.theme.ThemeOption +import org.futo.inputmethod.latin.uix.theme.UixThemeAuto import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.updates.DISABLE_UPDATE_REMINDER import org.futo.inputmethod.updates.autoDeferManualUpdateIfNeeded @@ -66,7 +68,6 @@ import org.futo.inputmethod.updates.openManualUpdateCheck import org.futo.inputmethod.updates.retrieveSavedLastUpdateCheckResult import java.util.Locale - private class LatinIMEActionInputTransaction( private val inputLogic: InputLogic, shouldApplySpace: Boolean, @@ -334,13 +335,13 @@ class UixManager(private val latinIME: LatinIME) { } } - val wordBeingForgotten: MutableState = mutableStateOf(null) - val forgetWordDismissed: MutableState = mutableStateOf(true) + private val wordBeingForgotten: MutableState = mutableStateOf(null) + private val forgetWordDismissed: MutableState = mutableStateOf(true) @Composable fun BoxScope.ForgetWordDialog() { AnimatedVisibility( - visible = forgetWordDismissed.value == false, + visible = !forgetWordDismissed.value, modifier = Modifier.matchParentSize(), enter = fadeIn(), exit = fadeOut() @@ -349,11 +350,13 @@ class UixManager(private val latinIME: LatinIME) { Box(modifier = Modifier.matchParentSize()) { Surface( color = Color.Black.copy(alpha = 0.66f), - modifier = Modifier.matchParentSize().pointerInput(Unit) { - this.detectTapGestures(onPress = { - forgetWordDismissed.value = true - }) - } + modifier = Modifier + .matchParentSize() + .pointerInput(Unit) { + this.detectTapGestures(onPress = { + forgetWordDismissed.value = true + }) + } ) { } Surface( @@ -405,6 +408,23 @@ class UixManager(private val latinIME: LatinIME) { } } + private var languageSwitcherDialog: DialogComposeView? = null + fun showLanguageSwitcher() { + // Dismiss old dialog + languageSwitcherDialog?.dismiss() + + // Create new dialog + languageSwitcherDialog = createDialogComposeView(latinIME) { + UixThemeAuto { + LanguageSwitcherDialog( + onDismiss = { it.dismiss() } + ) + } + } + + languageSwitcherDialog?.show() + } + fun setContent() { composeView?.setContent { UixThemeWrapper(latinIME.colorScheme) { @@ -522,6 +542,7 @@ class UixManager(private val latinIME: LatinIME) { fun onInputFinishing() { closeActionWindow() + languageSwitcherDialog?.dismiss() } fun cleanUpPersistentStates() { diff --git a/java/src/org/futo/inputmethod/latin/uix/Utils.kt b/java/src/org/futo/inputmethod/latin/uix/Utils.kt index 7a7bf939e..58580007f 100644 --- a/java/src/org/futo/inputmethod/latin/uix/Utils.kt +++ b/java/src/org/futo/inputmethod/latin/uix/Utils.kt @@ -1,8 +1,20 @@ package org.futo.inputmethod.latin.uix +import android.app.Dialog import android.content.Context import android.util.TypedValue +import android.view.Gravity +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import androidx.appcompat.content.res.AppCompatResources import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import org.futo.inputmethod.latin.LatinIME +import org.futo.inputmethod.latin.R import java.net.URLDecoder import java.net.URLEncoder @@ -29,4 +41,71 @@ fun String.urlEncode(): String { fun String.urlDecode(): String { return URLDecoder.decode(this, "utf-8") +} + + +// This ugly workaround is required as Android Compose freaks out when you use a Dialog outside of +// an activity (i.e. in an input method service) +data class DialogComposeView( + val dialog: Dialog, + val composeView: ComposeView +) + +fun createDialogComposeView( + latinIME: LatinIME, + maxWidthProportion: Float = 0.9f, + maxHeightProportion: Float = 0.75f, + dimAmount: Float = 0.5f, + onDismiss: () -> Unit = { }, + content: @Composable (Dialog) -> Unit, +): DialogComposeView { + val context: Context = latinIME + + val composeView = ComposeView(context) + + val dialog = Dialog(context).apply { + requestWindowFeature(Window.FEATURE_NO_TITLE) + setContentView(composeView) + + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + setOnDismissListener { + onDismiss() + } + } + + val window = dialog.window + window?.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM or WindowManager.LayoutParams.FLAG_DIM_BEHIND) + window?.attributes?.token = latinIME.latinIMELegacy.mKeyboardSwitcher.mainKeyboardView.windowToken + window?.attributes?.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG + + window?.apply { + val displayMetrics = context.resources.displayMetrics + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + + val maxWidth = (width * maxWidthProportion).toInt() + val maxHeight = (height * maxHeightProportion).toInt() + + setLayout(maxWidth, maxHeight) + setGravity(Gravity.CENTER) + + setBackgroundDrawable(AppCompatResources.getDrawable(context, R.drawable.empty)) + setDimAmount(dimAmount) + } + + composeView.setViewTreeLifecycleOwner(latinIME) + composeView.setViewTreeSavedStateRegistryOwner(latinIME) + + composeView.setContent { content(dialog) } + + return DialogComposeView(dialog, composeView) +} + +fun DialogComposeView.show() { + dialog.show() +} + +fun DialogComposeView.dismiss() { + dialog.dismiss() } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt b/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt index ab8728b1a..79368a827 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt @@ -20,6 +20,7 @@ val AllActions = listOf( RedoAction, VoiceInputAction, SystemVoiceInputAction, + SwitchLanguageAction ) @@ -70,7 +71,8 @@ val DefaultActions = listOf( ClipboardAction, SettingsAction, ThemeAction, - MemoryDebugAction + MemoryDebugAction, + SwitchLanguageAction ) val DefaultActionsString = ActionRegistry.actionsToString(DefaultActions) diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/SwitchLanguageAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/SwitchLanguageAction.kt new file mode 100644 index 000000000..2f20be335 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/actions/SwitchLanguageAction.kt @@ -0,0 +1,32 @@ +package org.futo.inputmethod.latin.uix.actions + +import android.content.Context +import org.futo.inputmethod.latin.ActiveSubtype +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.SubtypesSetting +import org.futo.inputmethod.latin.uix.Action +import org.futo.inputmethod.latin.uix.getSettingBlocking +import org.futo.inputmethod.latin.uix.setSettingBlocking + +fun switchToNextLanguage(context: Context) { + val enabledSubtypes = context.getSettingBlocking(SubtypesSetting).toList() + val currentSubtype = context.getSettingBlocking(ActiveSubtype) + + val index = enabledSubtypes.indexOf(currentSubtype) + val nextIndex = if(index == -1) { + 0 + } else { + (index + 1) % enabledSubtypes.size + } + + context.setSettingBlocking(ActiveSubtype.key, enabledSubtypes[nextIndex]) +} + +val SwitchLanguageAction = Action( + icon = R.drawable.globe, + name = R.string.show_language_switch_key, + simplePressImpl = { manager, _ -> + switchToNextLanguage(manager.getContext()) + }, + windowImpl = null, +) \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt index 5f898b82c..478f8243b 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt @@ -23,6 +23,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -539,3 +543,64 @@ fun SettingTextField(title: String, placeholder: String, field: SettingsKey DropDownPicker( + label: String, + options: List, + selection: T?, + onSet: (T) -> Unit, + getDisplayName: (T) -> String, + modifier: Modifier = Modifier +) { + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = modifier + ) { + TextField( + readOnly = true, + value = selection?.let(getDisplayName) ?: "None", + onValueChange = { }, + label = if (label.isNotBlank()) { + { Text(label) } + } else { + null + }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors( + focusedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer, + focusedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + focusedIndicatorColor = MaterialTheme.colorScheme.onPrimaryContainer, + focusedTrailingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.menuAnchor() + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + options.forEach { selectionOption -> + DropdownMenuItem( + text = { + Text(getDisplayName(selectionOption)) + }, + onClick = { + onSet(selectionOption) + expanded = false + } + ) + } + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt index 92b0cf0c0..1ccadbb22 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt @@ -11,6 +11,7 @@ import androidx.navigation.compose.rememberNavController import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.uix.ErrorDialog import org.futo.inputmethod.latin.uix.InfoDialog +import org.futo.inputmethod.latin.uix.settings.pages.AddLanguageScreen import org.futo.inputmethod.latin.uix.settings.pages.AdvancedParametersScreen import org.futo.inputmethod.latin.uix.settings.pages.BlacklistScreen import org.futo.inputmethod.latin.uix.settings.pages.CreditsScreen @@ -49,6 +50,7 @@ fun SettingsNavigator( ) { composable("home") { HomeScreen(navController) } composable("languages") { LanguagesScreen(navController) } + composable("addLanguage") { AddLanguageScreen(navController) } composable("predictiveText") { PredictiveTextScreen(navController) } composable("advancedparams") { AdvancedParametersScreen(navController) } composable("typing") { TypingScreen(navController) } diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/AddLanguage.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/AddLanguage.kt new file mode 100644 index 000000000..c27a2bf83 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/AddLanguage.kt @@ -0,0 +1,160 @@ +package org.futo.inputmethod.latin.uix.settings.pages + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import org.futo.inputmethod.latin.Subtypes +import org.futo.inputmethod.latin.uix.settings.DropDownPicker +import org.futo.inputmethod.latin.uix.settings.ScreenTitle +import org.futo.inputmethod.latin.uix.settings.ScrollableList +import org.futo.inputmethod.latin.uix.settings.SettingItem + +val QwertyVariants = listOf("qwerty", "qwertz", "dvorak", "azerty", "colemak", "bepo", "pcqwerty") + +val locales = mapOf( + "af" to listOf("qwerty"), + "ar" to listOf("arabic"), + "az_AZ" to listOf("qwerty"), + "be_BY" to listOf("east_slavic"), + "bg" to listOf("bulgarian"), + "bg" to listOf("bulgarian_bds"), + "bn_BD" to listOf("bengali_akkhor"), + "bn_IN" to listOf("bengali"), + "ca" to listOf("spanish"), + "cs" to listOf("qwertz"), + "da" to listOf("nordic"), + "de" to listOf("qwertz"), + "de_CH" to listOf("swiss"), + "el" to listOf("greek"), + "en_IN" to listOf("qwerty"), + "en_US" to QwertyVariants, + "en_GB" to QwertyVariants, + "eo" to listOf("spanish"), + "es" to listOf("spanish"), + "es_US" to listOf("spanish"), + "es_419" to listOf("spanish"), + "et_EE" to listOf("nordic"), + "eu_ES" to listOf("spanish"), + "fa" to listOf("farsi"), + "fi" to listOf("nordic"), + "fr" to listOf("azerty", "qwerty", "swiss", "bepo"), + "fr_CA" to listOf("qwerty", "azerty", "swiss", "bepo"), + "fr_CH" to listOf("swiss", "qwerty", "azerty", "bepo"), + "gl_ES" to listOf("spanish"), + "hi" to listOf("hindi", "hindi_compact"), + //"hi_ZZ" to listOf("qwerty"), + "hr" to listOf("qwertz"), + "hu" to listOf("qwertz"), + "hy_AM" to listOf("armenian_phonetic"), + "in" to listOf("qwerty"), + "is" to listOf("qwerty"), + "it" to listOf("qwerty"), + "it_CH" to listOf("swiss"), + "iw" to listOf("hebrew"), + "ka_GE" to listOf("georgian"), + "kk" to listOf("east_slavic"), + "km_KH" to listOf("khmer"), + "kn_IN" to listOf("kannada"), + "ky" to listOf("east_slavic"), + "lo_LA" to listOf("lao"), + "lt" to listOf("qwerty"), + "lv" to listOf("qwerty"), + "mk" to listOf("south_slavic"), + "ml_IN" to listOf("malayalam"), + "mn_MN" to listOf("mongolian"), + "mr_IN" to listOf("marathi"), + "ms_MY" to listOf("qwerty"), + "nb" to listOf("nordic"), + "ne_NP" to listOf("nepali_romanized", "nepali_traditional"), + "nl" to listOf("qwerty"), + "nl_BE" to listOf("azerty"), + "pl" to listOf("qwerty"), + "pt_BR" to listOf("qwerty"), + "pt_PT" to listOf("qwerty"), + "ro" to listOf("qwerty"), + "ru" to listOf("east_slavic"), + "si_LK" to listOf("sinhala"), + "sk" to listOf("qwerty"), + "sl" to listOf("qwerty"), + "sr" to listOf("south_slavic"), + "sr_ZZ" to listOf("serbian_qwertz"), + "sv" to listOf("nordic"), + "sw" to listOf("qwerty"), + "ta_IN" to listOf("tamil"), + "ta_LK" to listOf("tamil"), + "ta_SG" to listOf("tamil"), + "te_IN" to listOf("telugu"), + "th" to listOf("thai"), + "tl" to listOf("spanish"), + "tr" to listOf("qwerty"), + "uk" to listOf("east_slavic"), + "uz_UZ" to listOf("uzbek"), + "vi" to listOf("qwerty"), + "zu" to listOf("qwerty"), + "zz" to listOf("qwerty"), +) + +@Preview +@Composable +fun AddLanguageScreen(navController: NavHostController = rememberNavController()) { + val context = LocalContext.current + + val selectedLocale: MutableState = remember { mutableStateOf(context.resources.configuration.locale.toString()) } + val selectedLayout: MutableState = remember { mutableStateOf("qwerty") } + + val keys = remember { locales.keys.toList() } + ScrollableList { + ScreenTitle("Add Language", showBack = true, navController) + + SettingItem(title = "Language") { + DropDownPicker( + "", + keys, + selectedLocale.value, + { + selectedLocale.value = it + selectedLayout.value = locales[it]!!.first() + }, + { + Subtypes.getNameForLocale(it) + }, + modifier = Modifier.width(180.dp) + ) + } + + SettingItem(title = "Layout") { + DropDownPicker( + "", + locales[selectedLocale.value] ?: listOf(), + selectedLayout.value, + { selectedLayout.value = it }, + { Subtypes.getLayoutName(context, it) }, + modifier = Modifier.width(180.dp) + ) + } + + Button(onClick = { + Subtypes.addLanguage( + context, + Subtypes.getLocale(selectedLocale.value), + selectedLayout.value + ) + + navController.navigateUp() + }, modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Text("Add") + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Credits.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Credits.kt index 61d630154..407e0a93b 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Credits.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Credits.kt @@ -52,6 +52,11 @@ fun CreditsScreen(navController: NavHostController = rememberNavController()) { modifier = Modifier.clickable { context.openURI("https://github.com/gkonovalov/android-vad") }) + + ParagraphText("Some keyboard layouts were taken from the CyanogenMod/LineageOS fork of the LatinIME keyboard. Their fork is Apache-2.0 licensed. Copyright (C) 2015 The CyanogenMod Project", + modifier = Modifier.clickable { + context.openURI("https://github.com/LineageOS/android_packages_inputmethods_LatinIME") + }) Spacer(modifier = Modifier.height(16.dp)) ParagraphText("Note: The authors listed above are not affiliated with us and do not endorse or promote us") diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Languages.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Languages.kt index ee9925e80..1dddb3b42 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Languages.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Languages.kt @@ -1,32 +1,39 @@ package org.futo.inputmethod.latin.uix.settings.pages -import android.content.Context -import android.view.inputmethod.InputMethodManager +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -import okhttp3.internal.toImmutableList import org.futo.inputmethod.latin.BinaryDictionaryGetter import org.futo.inputmethod.latin.R -import org.futo.inputmethod.latin.RichInputMethodManager -import org.futo.inputmethod.latin.saveSubtypes +import org.futo.inputmethod.latin.Subtypes +import org.futo.inputmethod.latin.SubtypesSetting +import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.uix.FileKind import org.futo.inputmethod.latin.uix.ResourceHelper import org.futo.inputmethod.latin.uix.getSetting @@ -35,10 +42,10 @@ import org.futo.inputmethod.latin.uix.settings.NavigationItem import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle import org.futo.inputmethod.latin.uix.settings.ScreenTitle import org.futo.inputmethod.latin.uix.settings.ScrollableList -import org.futo.inputmethod.latin.uix.settings.Tip -import org.futo.inputmethod.latin.uix.settings.openLanguageSettings +import org.futo.inputmethod.latin.uix.settings.SettingItem +import org.futo.inputmethod.latin.uix.settings.useDataStoreValueBlocking +import org.futo.inputmethod.latin.uix.theme.Typography import org.futo.inputmethod.latin.uix.youAreImporting -import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils import org.futo.inputmethod.latin.xlm.ModelPaths import org.futo.inputmethod.updates.openURI import java.util.Locale @@ -101,27 +108,12 @@ fun LanguagesScreen(navController: NavHostController = rememberNavController()) val context = LocalContext.current val deleteDialogInfo: MutableState = remember { mutableStateOf(null) } - val inputMethodManager = remember { context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager } - val inputMethodList = remember { mutableStateOf( - inputMethodManager.getEnabledInputMethodSubtypeList( - RichInputMethodManager.getInstance().inputMethodInfoOfThisIme, - true - ).toImmutableList() - ) } - - val lifecycleOwner = LocalLifecycleOwner.current - val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState() - - LaunchedEffect(lifecycleState) { - delay(250L) - inputMethodList.value = inputMethodManager.getEnabledInputMethodSubtypeList( - RichInputMethodManager.getInstance().inputMethodInfoOfThisIme, - true - ) - - context.saveSubtypes() + val inputMethods = useDataStoreValueBlocking(SubtypesSetting) + val inputMethodList = remember(inputMethods) { + Subtypes.layoutsMappedByLanguage(inputMethods) } + val inputMethodKeys = remember(inputMethodList) { inputMethodList.keys.toList().sorted() } if(deleteDialogInfo.value != null) { val info = deleteDialogInfo.value!! @@ -139,17 +131,19 @@ fun LanguagesScreen(navController: NavHostController = rememberNavController()) ScreenTitle("Languages", showBack = true, navController) NavigationItem( - title = "Enable/disable languages", + title = "Add language", style = NavigationItemStyle.Misc, - navigate = { context.openLanguageSettings() }, + navigate = { + navController.navigate("addLanguage") + }, ) - Tip("Note: This screen is a WIP, use the above option to toggle languages") + inputMethodKeys.forEach { localeString -> + val subtypes = inputMethodList[localeString]!! - inputMethodList.value.forEach { - val name = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(it) + val locale = Subtypes.getLocale(localeString) - val locale = Locale.forLanguageTag(it.locale.replace("_", "-")) + val name = Subtypes.getName(subtypes.first()) val voiceInputModelName = ResourceHelper.tryFindingVoiceInputModelForLocale(context, locale)?.name?.let { stringResource(it) } val dictionaryName = runBlocking { ResourceHelper.findKeyForLocaleAndKind(context, locale, FileKind.Dictionary) }?.let { @@ -170,7 +164,36 @@ fun LanguagesScreen(navController: NavHostController = rememberNavController()) transformerModel = transformerName ) - ScreenTitle(name) + Row(modifier = Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.align(Alignment.CenterVertically).padding(0.dp, 16.dp)) { + Text(name, style = Typography.titleLarge) + if(subtypes.size == 1) { + val layout = Subtypes.getLayoutName(context, + subtypes.first().getExtraValueOf(Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET) ?: "" + ) + + Text(layout, + style = Typography.bodySmall, + color = MaterialTheme.colorScheme.outline) + } + } + + Spacer(modifier = Modifier.weight(1.0f)) + + if(subtypes.size == 1) { + IconButton(modifier = Modifier.fillMaxHeight().align(Alignment.CenterVertically), onClick = { + Subtypes.removeLanguage(context, subtypes.first()) + }) { + Icon( + Icons.Default.Clear, + contentDescription = "Remove language", + modifier = Modifier + ) + } + } + } NavigationItem( title = options.voiceInputModel ?: "None", @@ -208,6 +231,25 @@ fun LanguagesScreen(navController: NavHostController = rememberNavController()) }, icon = painterResource(id = R.drawable.cpu) ) + if(subtypes.size > 1) { + subtypes.forEach { + val layout = Subtypes.getLayoutName( + context, + it.getExtraValueOf(Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET) + ) + SettingItem(title = "Layout $layout") { + IconButton(modifier = Modifier.fillMaxHeight(), onClick = { + Subtypes.removeLanguage(context, it) + }) { + Icon( + Icons.Default.Clear, + contentDescription = "Remove layout $layout", + modifier = Modifier + ) + } + } + } + } } } diff --git a/java/src/org/futo/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/org/futo/inputmethod/latin/utils/DictionaryInfoUtils.java index eacb6f3d8..6f35112bf 100644 --- a/java/src/org/futo/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/org/futo/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -574,17 +574,6 @@ public class DictionaryInfoUtils { addOrUpdateDictInfo(dictList, dictionaryInfo); } - // Generate the dictionary information from the enabled subtypes. This will not - // overwrite the real records. - RichInputMethodManager.init(context); - List enabledSubtypes = RichInputMethodManager - .getInstance().getMyEnabledInputMethodSubtypeList(true); - for (InputMethodSubtype subtype : enabledSubtypes) { - Locale locale = LocaleUtils.constructLocaleFromString(subtype.getLocale()); - DictionaryInfo dictionaryInfo = createDictionaryInfoFromLocale(locale); - addOrUpdateDictInfo(dictList, dictionaryInfo); - } - return dictList; } diff --git a/java/src/org/futo/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/org/futo/inputmethod/latin/utils/SubtypeLocaleUtils.java index 43a10e227..da0248c1b 100644 --- a/java/src/org/futo/inputmethod/latin/utils/SubtypeLocaleUtils.java +++ b/java/src/org/futo/inputmethod/latin/utils/SubtypeLocaleUtils.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.inputmethod.InputMethodSubtype; import org.futo.inputmethod.latin.R; +import org.futo.inputmethod.latin.Subtypes; import org.futo.inputmethod.latin.common.LocaleUtils; import org.futo.inputmethod.latin.common.StringUtils; @@ -286,6 +287,19 @@ public final class SubtypeLocaleUtils { final String replacementString = getReplacementString(subtype, displayLocale); // TODO: rework this for multi-lingual subtypes final int nameResId = subtype.getNameResId(); + + if(nameResId == 0) { + if(replacementString.isEmpty()) { + return StringUtils.capitalizeFirstCodePoint( + Subtypes.INSTANCE.getLocale(subtype).getDisplayName(displayLocale), + displayLocale); + } else { + return StringUtils.capitalizeFirstCodePoint( + replacementString, + displayLocale); + } + } + final RunInLocale getSubtypeName = new RunInLocale() { @Override protected String job(final Resources res) {