From 263165b59697915566faf35df5586ea1838c3dc6 Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Fri, 1 Sep 2023 23:57:12 +0300 Subject: [PATCH] Creatre initial updated settings menu --- java/AndroidManifest.xml | 25 +- java/res/drawable/futo_logo.xml | 11 + java/res/xml/method.xml | 2 +- .../org/futo/inputmethod/latin/LatinIME.kt | 19 +- .../futo/inputmethod/latin/uix/Settings.kt | 31 +- .../latin/uix/actions/VoiceInputAction.kt | 26 +- .../latin/uix/settings/Components.kt | 295 ++++++++++++++++++ .../inputmethod/latin/uix/settings/Hooks.kt | 88 ++++++ .../latin/uix/settings/SettingsActivity.kt | 158 ++++++++++ .../latin/uix/settings/SettingsNavigator.kt | 26 ++ .../latin/uix/settings/SettingsUtils.kt | 37 +++ .../inputmethod/latin/uix/settings/Setup.kt | 147 +++++++++ .../latin/uix/settings/pages/Home.kt | 65 ++++ .../uix/settings/pages/PredictiveText.kt | 83 +++++ .../latin/uix/settings/pages/Typing.kt | 53 ++++ .../latin/uix/settings/pages/VoiceInput.kt | 129 ++++++++ 16 files changed, 1153 insertions(+), 42 deletions(-) create mode 100644 java/res/drawable/futo_logo.xml create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/Components.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index 4bc131b9c..b4084ab5a 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -87,12 +87,12 @@ - @@ -106,25 +106,6 @@ android:taskAffinity=""> - - - - - - - - - - - - + + + diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml index 151f87308..1f02f3b40 100644 --- a/java/res/xml/method.xml +++ b/java/res/xml/method.xml @@ -113,7 +113,7 @@ diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index 0441f34a8..b207b5079 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -185,12 +185,12 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun onCreate() { super.onCreate() - colorSchemeLoaderJob = deferGetSetting(THEME_KEY, DynamicSystemTheme.key) { - var themeKey = it - var themeOption = ThemeOptions[themeKey] - if (themeOption == null || !themeOption.available(this@LatinIME)) { - themeKey = VoiceInputTheme.key - themeOption = ThemeOptions[themeKey]!! + colorSchemeLoaderJob = deferGetSetting(THEME_KEY) { + val themeOptionFromSettings = ThemeOptions[it] + val themeOption = when { + themeOptionFromSettings == null -> VoiceInputTheme + !themeOptionFromSettings.available(this@LatinIME) -> VoiceInputTheme + else -> themeOptionFromSettings } activeThemeOption = themeOption @@ -418,11 +418,15 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun onFinishInputView(finishingInput: Boolean) { super.onFinishInputView(finishingInput) latinIMELegacy.onFinishInputView(finishingInput) + + closeActionWindow() } override fun onFinishInput() { super.onFinishInput() latinIMELegacy.onFinishInput() + + closeActionWindow() } override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) { @@ -440,6 +444,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun onWindowHidden() { super.onWindowHidden() latinIMELegacy.onWindowHidden() + + closeActionWindow() } override fun onUpdateSelection( @@ -636,6 +642,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } override fun closeActionWindow() { + if(currWindowActionWindow == null) return returnBackToMainKeyboardViewFromAction() } diff --git a/java/src/org/futo/inputmethod/latin/uix/Settings.kt b/java/src/org/futo/inputmethod/latin/uix/Settings.kt index a91cba25f..8011eb50e 100644 --- a/java/src/org/futo/inputmethod/latin/uix/Settings.kt +++ b/java/src/org/futo/inputmethod/latin/uix/Settings.kt @@ -6,7 +6,6 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers @@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme val Context.dataStore: DataStore by preferencesDataStore(name = "settings") @@ -28,6 +28,10 @@ suspend fun Context.getSetting(key: Preferences.Key, default: T): T { return valueFlow.first() } +fun Context.getSettingFlow(key: Preferences.Key, default: T): Flow { + return dataStore.data.map { preferences -> preferences[key] ?: default }.take(1) +} + suspend fun Context.setSetting(key: Preferences.Key, value: T) { this.dataStore.edit { preferences -> preferences[key] = value @@ -75,16 +79,27 @@ data class SettingsKey( ) suspend fun Context.getSetting(key: SettingsKey): T { - val valueFlow: Flow = - this.dataStore.data.map { preferences -> preferences[key.key] ?: key.default }.take(1) + return getSetting(key.key, key.default) +} - return valueFlow.first() +fun Context.getSettingFlow(key: SettingsKey): Flow { + return getSettingFlow(key.key, key.default) } suspend fun Context.setSetting(key: SettingsKey, value: T) { - this.dataStore.edit { preferences -> - preferences[key.key] = value - } + return setSetting(key.key, value) } -val THEME_KEY = stringPreferencesKey("activeThemeOption") \ No newline at end of file +fun LifecycleOwner.deferGetSetting(key: SettingsKey, onObtained: (T) -> Unit): Job { + return deferGetSetting(key.key, key.default, onObtained) +} + +fun LifecycleOwner.deferSetSetting(key: SettingsKey, value: T): Job { + return deferSetSetting(key.key, value) +} + + +val THEME_KEY = SettingsKey( + key = stringPreferencesKey("activeThemeOption"), + default = DynamicSystemTheme.key +) \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt index 45ca031eb..2aa629f12 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt @@ -3,7 +3,9 @@ package org.futo.inputmethod.latin.uix.actions import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -111,6 +113,7 @@ private class VoiceInputActionWindow( } private var recognizerView: MutableState = mutableStateOf(null) + private var modelException: MutableState = mutableStateOf(null) private val initJob = manager.getLifecycleScope().launch { yield() @@ -128,8 +131,7 @@ private class VoiceInputActionWindow( modelManager = state.modelManager ) } catch(e: ModelDoesNotExistException) { - // TODO: Show an error to the user, with an option to download - close() + modelException.value = e return@launch } @@ -151,6 +153,15 @@ private class VoiceInputActionWindow( return inputTransaction!! } + @Composable + private fun ModelDownloader(modelException: ModelDoesNotExistException) { + Column { + Text("Model Download Required") + Text("Not yet implemented") + // TODO + } + } + @Composable override fun windowName(): String { return stringResource(R.string.voice_input_action_title) @@ -167,7 +178,10 @@ private class VoiceInputActionWindow( indication = null, interactionSource = remember { MutableInteractionSource() })) { Box(modifier = Modifier.align(Alignment.Center)) { - recognizerView.value?.Content() + when { + modelException.value != null -> ModelDownloader(modelException.value!!) + recognizerView.value != null -> recognizerView.value!!.Content() + } } } } @@ -178,12 +192,14 @@ private class VoiceInputActionWindow( } private var wasFinished = false + private var cancelPlayed = false override fun cancelled() { if (!wasFinished) { - if (shouldPlaySounds) { + if (shouldPlaySounds && !cancelPlayed) { state.soundPlayer.playCancelSound() + cancelPlayed = true } - getOrStartInputTransaction().cancel() + inputTransaction?.cancel() } } diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt new file mode 100644 index 000000000..336cd6947 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt @@ -0,0 +1,295 @@ +package org.futo.inputmethod.latin.uix.settings + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.painter.Painter +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.uix.SettingsKey +import org.futo.inputmethod.latin.uix.theme.Typography + +@Composable +fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHostController = rememberNavController()) { + val rowModifier = if(showBack) { + Modifier + .fillMaxWidth() + .clickable { navController.popBackStack() } + } else { + Modifier.fillMaxWidth() + } + Row(modifier = rowModifier) { + Spacer(modifier = Modifier.width(16.dp)) + + if(showBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back", modifier = Modifier.align(CenterVertically)) + Spacer(modifier = Modifier.width(18.dp)) + } + Text(title, style = Typography.titleLarge, modifier = Modifier + .align(CenterVertically) + .padding(0.dp, 16.dp)) + } +} + +@Composable +@Preview +fun Tip(text: String = "This is an example tip") { + Surface( + color = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier + .fillMaxWidth() + .padding(8.dp), shape = RoundedCornerShape(4.dp) + ) { + Text( + text, + modifier = Modifier.padding(8.dp), + style = Typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } +} + + +@Composable +fun SettingItem( + title: String, + subtitle: String? = null, + onClick: () -> Unit, + icon: (@Composable () -> Unit)? = null, + disabled: Boolean = false, + content: @Composable () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(0.dp, 68.dp) + .clickable(enabled = !disabled, onClick = { + if (!disabled) { + onClick() + } + }) + .padding(0.dp, 4.dp, 0.dp, 4.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier + .width(48.dp) + .align(Alignment.CenterVertically) + ) { + Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { + if (icon != null) { + icon() + } + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + Row( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .alpha( + if (disabled) { + 0.5f + } else { + 1.0f + } + ) + ) { + Column { + Text(title, style = Typography.bodyLarge) + + if (subtitle != null) { + Text( + subtitle, + style = Typography.bodySmall, + color = MaterialTheme.colorScheme.outline + ) + } + } + } + Box(modifier = Modifier.align(Alignment.CenterVertically)) { + content() + } + + Spacer(modifier = Modifier.width(12.dp)) + } +} + +@Composable +fun SettingToggleRaw( + title: String, + enabled: Boolean, + setValue: (Boolean) -> Unit, + subtitle: String? = null, + disabled: Boolean = false, + icon: (@Composable () -> Unit)? = null +) { + SettingItem( + title = title, + subtitle = subtitle, + onClick = { + if (!disabled) { + setValue(!enabled) + } + }, + icon = icon + ) { + Switch(checked = enabled, onCheckedChange = { + if (!disabled) { + setValue(!enabled) + } + }, enabled = !disabled) + } +} + +@Composable +fun SettingToggleDataStoreItem( + title: String, + dataStoreItem: DataStoreItem, + subtitle: String? = null, + disabledSubtitle: String? = null, + disabled: Boolean = false, + icon: (@Composable () -> Unit)? = null +) { + val (enabled, setValue) = dataStoreItem + + val subtitleValue = if (!enabled && disabledSubtitle != null) { + disabledSubtitle + } else { + subtitle + } + + SettingToggleRaw(title, enabled, { setValue(it) }, subtitleValue, disabled, icon) +} + +@Composable +fun SettingToggleDataStore( + title: String, + setting: SettingsKey, + subtitle: String? = null, + disabledSubtitle: String? = null, + disabled: Boolean = false, + icon: (@Composable () -> Unit)? = null +) { + SettingToggleDataStoreItem( + title, useDataStore(setting.key, setting.default), subtitle, disabledSubtitle, disabled, icon) +} + +@Composable +fun SettingToggleSharedPrefs( + title: String, + key: String, + default: Boolean, + subtitle: String? = null, + disabledSubtitle: String? = null, + disabled: Boolean = false, + icon: (@Composable () -> Unit)? = null +) { + SettingToggleDataStoreItem( + title, useSharedPrefsBool(key, default), subtitle, disabledSubtitle, disabled, icon) +} + +@Composable +fun ScrollableList(content: @Composable () -> Unit) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + content() + } +} + +@Composable +fun SettingListLazy(content: LazyListScope.() -> Unit) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + ) { + content() + } +} + + +enum class NavigationItemStyle { + HomePrimary, + HomeSecondary, + HomeTertiary, + Misc +} + +@Composable +fun NavigationItem(title: String, style: NavigationItemStyle, navigate: () -> Unit, icon: Painter? = null) { + SettingItem( + title = title, + onClick = navigate, + icon = { + icon?.let { + val circleColor = when(style) { + NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.primaryContainer + NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.secondaryContainer + NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.tertiaryContainer + NavigationItemStyle.Misc -> Color.Transparent + } + + val iconColor = when(style) { + NavigationItemStyle.HomePrimary -> MaterialTheme.colorScheme.onPrimaryContainer + NavigationItemStyle.HomeSecondary -> MaterialTheme.colorScheme.onSecondaryContainer + NavigationItemStyle.HomeTertiary -> MaterialTheme.colorScheme.onTertiaryContainer + NavigationItemStyle.Misc -> MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f) + } + + Canvas(modifier = Modifier.fillMaxSize()) { + drawCircle(circleColor, this.size.maxDimension / 2.4f) + translate( + left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f, + top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f + ) { + with(icon) { + draw(icon.intrinsicSize, colorFilter = ColorFilter.tint(iconColor)) + } + } + } + } + } + ) { + when(style) { + NavigationItemStyle.Misc -> Icon(Icons.Default.ArrowForward, contentDescription = "Go") + else -> {} + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt new file mode 100644 index 000000000..22799f06e --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Hooks.kt @@ -0,0 +1,88 @@ +package org.futo.inputmethod.latin.uix.settings + +import android.content.SharedPreferences +import android.preference.PreferenceManager +import android.provider.Settings +import android.view.inputmethod.InputMethodManager +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.edit +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.futo.inputmethod.latin.uix.dataStore + +data class DataStoreItem(val value: T, val setValue: (T) -> Job) +@Composable +fun useDataStore(key: Preferences.Key, default: T): DataStoreItem { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val enableSoundFlow: Flow = remember { + context.dataStore.data.map { + preferences -> preferences[key] ?: default + } + } + + val value = enableSoundFlow.collectAsState(initial = default).value!! + + val setValue = { newValue: T -> + coroutineScope.launch { + context.dataStore.edit { preferences -> + preferences[key] = newValue + } + } + } + + return DataStoreItem(value, setValue) +} + +@Composable +fun useSharedPrefsBool(key: String, default: Boolean): DataStoreItem { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val sharedPrefs = remember { PreferenceManager.getDefaultSharedPreferences(context) } + + val value = remember { mutableStateOf(sharedPrefs.getBoolean(key, default)) } + + // This is not the most efficient way to do this... but it works for a settings menu + DisposableEffect(Unit) { + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, changedKey -> + if (key == changedKey) { + value.value = sharedPreferences.getBoolean(key, value.value) + } + } + + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + + onDispose { + sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) + } + } + + val setValue = { newValue: Boolean -> + coroutineScope.launch { + withContext(Dispatchers.Main) { + sharedPrefs.edit { + putBoolean(key, newValue) + } + } + } + } + + return DataStoreItem(value.value, setValue) +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt new file mode 100644 index 000000000..de087f970 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsActivity.kt @@ -0,0 +1,158 @@ +package org.futo.inputmethod.latin.uix.settings + +import android.content.Context +import android.content.Context.INPUT_METHOD_SERVICE +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import android.view.inputmethod.InputMethodManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.futo.inputmethod.latin.uix.THEME_KEY +import org.futo.inputmethod.latin.uix.deferGetSetting +import org.futo.inputmethod.latin.uix.theme.ThemeOption +import org.futo.inputmethod.latin.uix.theme.ThemeOptions +import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper +import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme + +private fun Context.isInputMethodEnabled(): Boolean { + val packageName = packageName + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + + var found = false + for (imi in imm.enabledInputMethodList) { + if (packageName == imi.packageName) { + found = true + } + } + + return found +} + +private fun Context.isDefaultIMECurrent(): Boolean { + val value = Settings.Secure.getString(contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD) + + return value.startsWith(packageName) +} + + +class SettingsActivity : ComponentActivity() { + private val themeOption: MutableState = mutableStateOf(null) + + private val inputMethodEnabled = mutableStateOf(false) + private val inputMethodSelected = mutableStateOf(false) + + private var wasImeEverDisabled = false + + companion object { + private var pollJob: Job? = null + } + + @OptIn(DelicateCoroutinesApi::class) + private fun updateSystemState() { + val inputMethodEnabled = isInputMethodEnabled() + val inputMethodSelected = isDefaultIMECurrent() + this.inputMethodEnabled.value = inputMethodEnabled + this.inputMethodSelected.value = inputMethodSelected + + if(!inputMethodEnabled) { + wasImeEverDisabled = true + } else if(wasImeEverDisabled) { + // We just went from inputMethodEnabled==false to inputMethodEnabled==true + // This is because the user is in the input method settings screen and just turned on + // our IME. We can bring them back here so that they don't have to press back button + wasImeEverDisabled = false + + val intent = Intent() + intent.setClass(this, SettingsActivity::class.java) + intent.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + or Intent.FLAG_ACTIVITY_SINGLE_TOP + or Intent.FLAG_ACTIVITY_CLEAR_TOP) + + startActivity(intent) + } + + if(!inputMethodEnabled || !inputMethodSelected) { + if(pollJob == null || !pollJob!!.isActive) { + pollJob = GlobalScope.launch { + systemStatePoller() + } + } + } + } + + private suspend fun systemStatePoller() { + while(!this.inputMethodEnabled.value || !this.inputMethodSelected.value) { + delay(200) + updateSystemState() + } + } + + private fun updateContent() { + setContent { + themeOption.value?.let { themeOption -> + val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key) + val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption + UixThemeWrapper(theme.obtainColors(LocalContext.current)) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + SetupOrMain(inputMethodEnabled.value, inputMethodSelected.value) { + SettingsNavigator() + } + } + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + updateSystemState() + updateContent() + } + } + + deferGetSetting(THEME_KEY) { + val themeOptionFromSettings = ThemeOptions[it] + val themeOption = when { + themeOptionFromSettings == null -> VoiceInputTheme + !themeOptionFromSettings.available(this) -> VoiceInputTheme + else -> themeOptionFromSettings + } + + this.themeOption.value = themeOption + } + } + + override fun onResume() { + super.onResume() + + updateSystemState() + } + + override fun onRestart() { + super.onRestart() + + updateSystemState() + } +} \ 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 new file mode 100644 index 000000000..d1c5da1d7 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsNavigator.kt @@ -0,0 +1,26 @@ +package org.futo.inputmethod.latin.uix.settings + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.futo.inputmethod.latin.uix.settings.pages.HomeScreen +import org.futo.inputmethod.latin.uix.settings.pages.PredictiveTextScreen +import org.futo.inputmethod.latin.uix.settings.pages.TypingScreen +import org.futo.inputmethod.latin.uix.settings.pages.VoiceInputScreen + +@Composable +fun SettingsNavigator( + navController: NavHostController = rememberNavController() +) { + NavHost( + navController = navController, + startDestination = "home" + ) { + composable("home") { HomeScreen(navController) } + composable("predictiveText") { PredictiveTextScreen(navController) } + composable("typing") { TypingScreen(navController) } + composable("voiceInput") { VoiceInputScreen(navController) } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt new file mode 100644 index 000000000..d15ec490c --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/SettingsUtils.kt @@ -0,0 +1,37 @@ +package org.futo.inputmethod.latin.uix.settings + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.view.inputmethod.InputMethodManager +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import org.futo.inputmethod.latin.utils.UncachedInputMethodManagerUtils + +@Composable +fun SetupOrMain(inputMethodEnabled: Boolean, inputMethodSelected: Boolean, main: @Composable () -> Unit) { + if (!inputMethodEnabled) { + SetupEnableIME() + } else if (!inputMethodSelected) { + SetupChangeDefaultIME() + } else { + main() + } +} + +// TODO: We should have one central source of enabled languages to share between +// keyboard and voice input. We need to pass current language to voice input action +// and restrict it to that when possible. If active language has no voice input support +// we must tell the user in the UI. +fun Context.openLanguageSettings() { + val imm = getSystemService(ComponentActivity.INPUT_METHOD_SERVICE) as InputMethodManager + + val imi = UncachedInputMethodManagerUtils.getInputMethodInfoOf( + packageName, imm + ) ?: return + val intent = Intent() + intent.action = Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.id) + startActivity(intent) +} diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt new file mode 100644 index 000000000..620d6476d --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Setup.kt @@ -0,0 +1,147 @@ +package org.futo.inputmethod.latin.uix.settings + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.view.inputmethod.InputMethodManager +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.uix.theme.Typography + +@Composable +fun SetupContainer(inner: @Composable () -> Unit) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth(fraction = 1.0f) + .fillMaxHeight(fraction = 0.4f) + ) { + Icon( + painter = painterResource(id = R.drawable.futo_logo), + contentDescription = "FUTO Logo", + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(0.75f) + .align(Alignment.CenterHorizontally), + tint = MaterialTheme.colorScheme.onBackground + ) + } + + Row( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(fraction = 0.5f) + .align(Alignment.CenterVertically) + .padding(32.dp) + ) { + Box(modifier = Modifier.align(Alignment.CenterVertically)) { + inner() + } + } + } + } +} + + +@Composable +fun Step(fraction: Float, text: String) { + Column(modifier = Modifier.padding(16.dp)) { + Text(text, style = Typography.labelSmall) + LinearProgressIndicator(progress = fraction, modifier = Modifier.fillMaxWidth()) + } +} + +// TODO: May wish to have a skip option +@Composable +@Preview +fun SetupEnableIME() { + val context = LocalContext.current + + val launchImeOptions = { + // TODO: look into direct boot to get rid of direct boot warning? + val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS) + + intent.flags = (Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + or Intent.FLAG_ACTIVITY_NO_HISTORY + or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + + context.startActivity(intent) + } + + SetupContainer { + Column { + Step(fraction = 1.0f/3.0f, text = "Setup - Step 1 of 2") + + Text( + "To use FUTO Keyboard, you must first enable FUTO Keyboard as an input method.", + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Button( + onClick = launchImeOptions, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text("Open Input Method Settings") + } + } + } +} + + +@Composable +@Preview +fun SetupChangeDefaultIME() { + val context = LocalContext.current + + val launchImeOptions = { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + inputMethodManager.showInputMethodPicker() + } + + SetupContainer { + Column { + Step(fraction = 2.0f/3.0f, text = "Setup - Step 2 of 2") + + Text( + "Next, select FUTO Keyboard as your active input method.", + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Button( + onClick = launchImeOptions, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text("Switch Input Methods") + } + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt new file mode 100644 index 000000000..cc1dffd2c --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt @@ -0,0 +1,65 @@ +package org.futo.inputmethod.latin.uix.settings.pages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import org.futo.inputmethod.latin.R +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.openLanguageSettings + +@Preview +@Composable +fun HomeScreen(navController: NavHostController = rememberNavController()) { + val context = LocalContext.current + ScrollableList { + ScreenTitle("FUTO Keyboard Settings") + NavigationItem( + title = "Languages", + style = NavigationItemStyle.HomePrimary, + navigate = { context.openLanguageSettings() }, + icon = painterResource(id = R.drawable.globe) + ) + + NavigationItem( + title = "Predictive Text", + style = NavigationItemStyle.HomeSecondary, + navigate = { navController.navigate("predictiveText") }, + icon = painterResource(id = R.drawable.shift) + ) + + NavigationItem( + title = "Typing Preferences", + style = NavigationItemStyle.HomeSecondary, + navigate = { navController.navigate("typing") }, + icon = painterResource(id = R.drawable.delete) + ) + + NavigationItem( + title = "Voice Input", + style = NavigationItemStyle.HomeSecondary, + navigate = { navController.navigate("voiceInput") }, + icon = painterResource(id = R.drawable.mic_fill) + ) + + NavigationItem( + title = "Theme", + style = NavigationItemStyle.HomeTertiary, + navigate = { /* TODO */ }, + icon = painterResource(id = R.drawable.eye) + ) + + NavigationItem( + title = "Advanced", + style = NavigationItemStyle.Misc, + navigate = { /* TODO */ }, + icon = painterResource(id = R.drawable.delete) + ) + + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt new file mode 100644 index 000000000..767721a46 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/PredictiveText.kt @@ -0,0 +1,83 @@ +package org.futo.inputmethod.latin.uix.settings.pages + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.booleanResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import org.futo.inputmethod.dictionarypack.DictionarySettingsActivity +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.settings.Settings +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.SettingToggleSharedPrefs +import org.futo.inputmethod.latin.uix.settings.Tip + +@Preview +@Composable +fun PredictiveTextScreen(navController: NavHostController = rememberNavController()) { + val context = LocalContext.current + ScrollableList { + ScreenTitle("Predictive Text", showBack = true, navController) + + Tip("Note: Transformer LM is not yet finished, the prediction algorithm is still the default AOSP Keyboard prediction algorithm") + + NavigationItem( + title = stringResource(R.string.edit_personal_dictionary), + style = NavigationItemStyle.Misc, + navigate = { + val intent = Intent("android.settings.USER_DICTIONARY_SETTINGS") + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } + ) + + NavigationItem( + title = stringResource(R.string.configure_dictionaries_title), + style = NavigationItemStyle.Misc, + navigate = { + val intent = Intent() + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.setClass(context, DictionarySettingsActivity::class.java) + intent.putExtra("clientId", "org.futo.inputmethod.latin") + context.startActivity(intent) + } + ) + + SettingToggleSharedPrefs( + title = stringResource(R.string.prefs_block_potentially_offensive_title), + subtitle = stringResource(R.string.prefs_block_potentially_offensive_summary), + key = Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, + default = booleanResource(R.bool.config_block_potentially_offensive) + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.auto_correction), + subtitle = stringResource(R.string.auto_correction_summary), + key = Settings.PREF_AUTO_CORRECTION, + default = true + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.prefs_show_suggestions), + subtitle = stringResource(R.string.prefs_show_suggestions_summary), + key = Settings.PREF_SHOW_SUGGESTIONS, + default = true + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.use_personalized_dicts), + subtitle = stringResource(R.string.use_personalized_dicts_summary), + key = Settings.PREF_KEY_USE_PERSONALIZED_DICTS, + default = true + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.bigram_prediction), + subtitle = stringResource(R.string.bigram_prediction_summary), + key = Settings.PREF_BIGRAM_PREDICTIONS, + default = booleanResource(R.bool.config_default_next_word_prediction) + ) + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt new file mode 100644 index 000000000..78d0e1b08 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt @@ -0,0 +1,53 @@ +package org.futo.inputmethod.latin.uix.settings.pages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.booleanResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.settings.Settings +import org.futo.inputmethod.latin.uix.settings.ScreenTitle +import org.futo.inputmethod.latin.uix.settings.ScrollableList +import org.futo.inputmethod.latin.uix.settings.SettingToggleSharedPrefs + +@Preview +@Composable +fun TypingScreen(navController: NavHostController = rememberNavController()) { + val context = LocalContext.current + ScrollableList { + ScreenTitle("Typing Preferences", showBack = true, navController) + + SettingToggleSharedPrefs( + title = stringResource(R.string.auto_cap), + subtitle = stringResource(R.string.auto_cap_summary), + key = Settings.PREF_AUTO_CAP, + default = true + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.use_double_space_period), + subtitle = stringResource(R.string.use_double_space_period_summary), + key = Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, + default = true + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.vibrate_on_keypress), + key = Settings.PREF_VIBRATE_ON, + default = booleanResource(R.bool.config_default_vibration_enabled) + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.sound_on_keypress), + key = Settings.PREF_SOUND_ON, + default = booleanResource(R.bool.config_default_sound_enabled) + ) + SettingToggleSharedPrefs( + title = stringResource(R.string.popup_on_keypress), + key = Settings.PREF_POPUP_ON, + default = booleanResource(R.bool.config_default_key_preview_popup) + ) + + // TODO: SeekBarDialogPreference pref_vibration_duration_settings etc + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt new file mode 100644 index 000000000..bcd0f7421 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt @@ -0,0 +1,129 @@ +package org.futo.inputmethod.latin.uix.settings.pages + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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 org.futo.inputmethod.latin.uix.DISALLOW_SYMBOLS +import org.futo.inputmethod.latin.uix.ENABLE_SOUND +import org.futo.inputmethod.latin.uix.ENGLISH_MODEL_INDEX +import org.futo.inputmethod.latin.uix.SettingsKey +import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS +import org.futo.inputmethod.latin.uix.settings.ScreenTitle +import org.futo.inputmethod.latin.uix.settings.ScrollableList +import org.futo.inputmethod.latin.uix.settings.SettingToggleDataStore +import org.futo.inputmethod.latin.uix.settings.useDataStore +import org.futo.voiceinput.shared.ENGLISH_MODELS +import org.futo.voiceinput.shared.types.ModelLoader + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ModelPicker(label: String, options: List, setting: SettingsKey) { + val (modelIndex, setModelIndex) = useDataStore(key = setting.key, default = setting.default) + + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.align(Alignment.Center) + ) { + TextField( + readOnly = true, + value = stringResource(options[modelIndex].name), + onValueChange = { }, + label = { Text(label) }, + 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.forEachIndexed { i, selectionOption -> + DropdownMenuItem( + text = { + Text(stringResource(selectionOption.name)) + }, + onClick = { + setModelIndex(i) + expanded = false + } + ) + } + } + } + } +} + + + +@Preview +@Composable +fun VoiceInputScreen(navController: NavHostController = rememberNavController()) { + val context = LocalContext.current + ScrollableList { + ScreenTitle("Voice Input", showBack = true, navController) + + SettingToggleDataStore( + title = "Indication sounds", + subtitle = "Play sounds on start and cancel", + setting = ENABLE_SOUND + ) + + SettingToggleDataStore( + title = "Verbose progress", + subtitle = "Display verbose information about model inference", + setting = VERBOSE_PROGRESS + ) + + SettingToggleDataStore( + title = "Suppress symbols", + setting = DISALLOW_SYMBOLS + ) + + ModelPicker( + "English Model Option", + ENGLISH_MODELS, + ENGLISH_MODEL_INDEX + ) + } +} \ No newline at end of file