Creatre initial updated settings menu

This commit is contained in:
Aleksandras Kostarevas 2023-09-01 23:57:12 +03:00
parent 7f656bb622
commit 263165b596
16 changed files with 1153 additions and 42 deletions

View File

@ -87,12 +87,12 @@
</service>
<!-- Activities -->
<activity android:name=".setup.SetupActivity"
android:theme="@style/platformActivityTheme"
<activity android:name=".uix.settings.SettingsActivity"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:label="@string/english_ime_name"
android:icon="@drawable/ic_launcher_keyboard"
android:launchMode="singleTask"
android:noHistory="true"
android:noHistory="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@ -106,25 +106,6 @@
android:taskAffinity="">
</activity>
<activity android:name=".setup.SetupWizardActivity"
android:theme="@style/platformActivityTheme"
android:label="@string/english_ime_name"
android:clearTaskOnLaunch="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity android:name=".settings.SettingsActivity"
android:theme="@style/platformSettingsTheme"
android:label="@string/english_ime_settings"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity android:name=".spellcheck.SpellCheckerSettingsActivity"
android:theme="@style/platformSettingsTheme"
android:label="@string/android_spell_checker_settings"

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="92dp"
android:height="24dp"
android:viewportWidth="92"
android:viewportHeight="24">
<path
android:pathData="M91.636,12C91.636,18.627 86.267,24 79.644,24C73.02,24 67.651,18.627 67.651,12C67.651,5.373 73.02,0 79.644,0C86.267,0 91.636,5.373 91.636,12ZM76.15,14.422C74.92,13.191 74.305,12.575 74.305,11.811C74.305,11.046 74.92,10.431 76.15,9.2L77.153,8.197C78.383,6.966 78.998,6.351 79.762,6.351C80.526,6.351 81.141,6.966 82.371,8.197L83.374,9.2C84.604,10.431 85.219,11.046 85.219,11.811C85.219,12.575 84.604,13.191 83.374,14.422L82.371,15.425C81.141,16.655 80.526,17.271 79.762,17.271C78.998,17.271 78.383,16.655 77.153,15.425L76.15,14.422ZM16.913,7.077C17.252,7.077 17.528,6.801 17.528,6.462V1.846C17.528,1.506 17.252,1.231 16.913,1.231H0.615C0.275,1.231 0,1.506 0,1.846V22.154C0,22.494 0.275,22.769 0.615,22.769H6.15C6.49,22.769 6.765,22.494 6.765,22.154V16.492C6.765,16.152 7.04,15.877 7.38,15.877H14.822C15.161,15.877 15.437,15.601 15.437,15.262V10.646C15.437,10.306 15.161,10.031 14.822,10.031H7.38C7.04,10.031 6.765,9.755 6.765,9.415V7.692C6.765,7.352 7.04,7.077 7.38,7.077H16.913ZM31.209,23.139H31.302C37.882,23.139 41.91,19.631 41.91,12.954V1.846C41.91,1.506 41.635,1.231 41.295,1.231H35.76C35.421,1.231 35.145,1.506 35.145,1.846V12.339C35.145,14.615 34.161,16.615 31.302,16.615H31.209C28.38,16.615 27.365,14.615 27.365,12.339V1.846C27.365,1.506 27.09,1.231 26.75,1.231H21.215C20.876,1.231 20.6,1.506 20.6,1.846V12.954C20.6,19.631 24.629,23.139 31.209,23.139ZM44.985,1.846C44.985,1.506 45.26,1.231 45.599,1.231H65.464C65.804,1.231 66.079,1.506 66.079,1.846V6.554C66.079,6.894 65.804,7.169 65.464,7.169H59.529C59.19,7.169 58.915,7.445 58.915,7.785V22.154C58.915,22.494 58.639,22.769 58.299,22.769H52.764C52.425,22.769 52.149,22.494 52.149,22.154V7.785C52.149,7.445 51.874,7.169 51.534,7.169H45.599C45.26,7.169 44.985,6.894 44.985,6.554V1.846Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -113,7 +113,7 @@
<!-- If IME doesn't have an applicable subtype, the first subtype will be used as a default
subtype.-->
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="org.futo.inputmethod.latin.settings.SettingsActivity"
android:settingsActivity="org.futo.inputmethod.latin.uix.settings.SettingsActivity"
android:isDefault="@bool/im_is_default"
android:supportsSwitchingToNextInputMethod="true"
android:supportsInlineSuggestions="true">

View File

@ -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()
}

View File

@ -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<Preferences> by preferencesDataStore(name = "settings")
@ -28,6 +28,10 @@ suspend fun <T> Context.getSetting(key: Preferences.Key<T>, default: T): T {
return valueFlow.first()
}
fun <T> Context.getSettingFlow(key: Preferences.Key<T>, default: T): Flow<T> {
return dataStore.data.map { preferences -> preferences[key] ?: default }.take(1)
}
suspend fun <T> Context.setSetting(key: Preferences.Key<T>, value: T) {
this.dataStore.edit { preferences ->
preferences[key] = value
@ -75,16 +79,27 @@ data class SettingsKey<T>(
)
suspend fun <T> Context.getSetting(key: SettingsKey<T>): T {
val valueFlow: Flow<T> =
this.dataStore.data.map { preferences -> preferences[key.key] ?: key.default }.take(1)
return getSetting(key.key, key.default)
}
return valueFlow.first()
fun <T> Context.getSettingFlow(key: SettingsKey<T>): Flow<T> {
return getSettingFlow(key.key, key.default)
}
suspend fun <T> Context.setSetting(key: SettingsKey<T>, value: T) {
this.dataStore.edit { preferences ->
preferences[key.key] = value
}
return setSetting(key.key, value)
}
val THEME_KEY = stringPreferencesKey("activeThemeOption")
fun <T> LifecycleOwner.deferGetSetting(key: SettingsKey<T>, onObtained: (T) -> Unit): Job {
return deferGetSetting(key.key, key.default, onObtained)
}
fun <T> LifecycleOwner.deferSetSetting(key: SettingsKey<T>, value: T): Job {
return deferSetSetting(key.key, value)
}
val THEME_KEY = SettingsKey(
key = stringPreferencesKey("activeThemeOption"),
default = DynamicSystemTheme.key
)

View File

@ -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<RecognizerView?> = mutableStateOf(null)
private var modelException: MutableState<ModelDoesNotExistException?> = 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()
}
}

View File

@ -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<Boolean>,
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<Boolean>,
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 -> {}
}
}
}

View File

@ -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<T>(val value: T, val setValue: (T) -> Job)
@Composable
fun <T> useDataStore(key: Preferences.Key<T>, default: T): DataStoreItem<T> {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val enableSoundFlow: Flow<T> = 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<Boolean> {
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)
}

View File

@ -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<ThemeOption?> = 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()
}
}

View File

@ -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) }
}
}

View File

@ -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)
}

View File

@ -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")
}
}
}
}

View File

@ -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)
)
}
}

View File

@ -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)
)
}
}

View File

@ -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
}
}

View File

@ -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<ModelLoader>, setting: SettingsKey<Int>) {
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
)
}
}