add new settings with search functionality (WIP)

This commit is contained in:
Helium314 2025-01-25 22:07:56 +01:00
parent fc12877795
commit e1be94112e
21 changed files with 1539 additions and 11 deletions

View file

@ -105,7 +105,6 @@ __Planned features and improvements:__
* [Bug fixes](https://github.com/Helium314/HeliBoard/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
__What will _not_ be added:__
* Material 3 (not worth adding 1.5 MB to app size)
* Dictionaries for more languages (you can still download them)
* Anything that requires additional permissions, unless there is a _very_ good reason

View file

@ -2,6 +2,7 @@ plugins {
id("com.android.application")
kotlin("android")
kotlin("plugin.serialization") version "2.0.21"
kotlin("plugin.compose") version "2.0.0"
}
android {
@ -49,6 +50,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
compose = true
}
externalNativeBuild {
@ -105,6 +107,14 @@ dependencies {
// kotlin
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
// compose
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation(platform("androidx.compose:compose-bom:2024.10.01"))
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.navigation:navigation-compose:2.8.5")
// color picker for user-defined colors
implementation("com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0")

View file

@ -74,10 +74,9 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
</intent-filter>
</activity>
<activity android:name=".settings.SettingsActivity"
<activity android:name="helium314.keyboard.settings.SettingsActivity2"
android:theme="@style/platformActivityTheme"
android:label="@string/ime_settings"
android:excludeFromRecents="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -89,6 +89,7 @@ import helium314.keyboard.latin.utils.StatsUtilsManager;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeSettingsKt;
import helium314.keyboard.latin.utils.ViewLayoutUtils;
import helium314.keyboard.settings.AllPrefsKt;
import kotlin.collections.CollectionsKt;
import java.io.FileDescriptor;
@ -879,6 +880,8 @@ public class LatinIME extends InputMethodService implements
void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
super.onStartInput(editorInfo, restarting);
reloadIfNecessary();
final List<Locale> hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo);
if (hintLocales == null) {
return;
@ -1976,4 +1979,12 @@ public class LatinIME extends InputMethodService implements
// deallocateMemory always called on hiding, and should not be called when showing
}
}
private void reloadIfNecessary() {
// better do the reload when showing the keyboard next time, and not on settings change
if (AllPrefsKt.themeChanged) {
mKeyboardSwitcher.forceUpdateKeyboardTheme(mDisplayContext);
AllPrefsKt.themeChanged = false;
}
}
}

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin
import android.content.Context

View file

@ -389,8 +389,13 @@ public class SettingsValues {
}
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
return prefs.getBoolean(Settings.PREF_USE_CONTACTS, false)
&& PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS);
final boolean setting = prefs.getBoolean(Settings.PREF_USE_CONTACTS, false);
if (!setting) return false;
if (PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
return true;
// disable if permission not granted
prefs.edit().putBoolean(Settings.PREF_USE_CONTACTS, false).apply();
return false;
}
public String dump() {

View file

@ -32,6 +32,7 @@ import helium314.keyboard.latin.utils.JniUtils;
import helium314.keyboard.latin.utils.LeakGuardHandlerWrapper;
import helium314.keyboard.latin.utils.ResourceUtils;
import helium314.keyboard.latin.utils.UncachedInputMethodManagerUtils;
import helium314.keyboard.settings.SettingsActivity2;
import java.util.ArrayList;
@ -104,10 +105,9 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActionBar actionBar = getSupportActionBar();
if (actionBar == null) {
return;
if (actionBar != null) {
actionBar.hide();
}
actionBar.hide();
getWindow().setStatusBarColor(getResources().getColor(R.color.setup_background));
ActivityThemeUtils.setActivityTheme(this);
@ -166,7 +166,7 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
0 /* finishedInstruction */, R.drawable.sym_keyboard_language_switch,
R.string.setup_step3_action);
step3.setAction(() -> {
final Intent intent = new Intent(getApplicationContext(), SettingsActivity.class);
final Intent intent = new Intent(getApplicationContext(), SettingsActivity2.class);
intent.setAction(Intent.ACTION_VIEW);
startActivity(intent);
finish();
@ -224,7 +224,7 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
private void invokeSettingsOfThisIme() {
final Intent intent = new Intent();
intent.setClass(this, SettingsActivity.class);
intent.setClass(this, SettingsActivity2.class);
intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,

View file

@ -27,6 +27,10 @@ public final class DeviceProtectedUtils {
return prefs;
}
Context deviceProtectedContext = getDeviceProtectedContext(context);
if (deviceProtectedContext == null) { // not relevant in practice, but happens when compose previews access shared preferences
prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs;
}
prefs = PreferenceManager.getDefaultSharedPreferences(deviceProtectedContext);
if (prefs.getAll().isEmpty()) {
Log.i(TAG, "Device encrypted storage is empty, copying values from credential encrypted storage");

View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import helium314.keyboard.latin.R
@Composable
fun AboutScreen(
onClickBack: () -> Unit,
) {
SearchPrefScreen(
onClickBack = onClickBack,
title = stringResource(R.string.settings_screen_about),
) {
SettingsActivity2.allPrefs.map[NonSettingsPrefs.APP]!!.Preference()
SettingsActivity2.allPrefs.map[NonSettingsPrefs.VERSION]!!.Preference()
SettingsActivity2.allPrefs.map[NonSettingsPrefs.LICENSE]!!.Preference()
SettingsActivity2.allPrefs.map[NonSettingsPrefs.HIDDEN_FEATURES]!!.Preference()
SettingsActivity2.allPrefs.map[NonSettingsPrefs.GITHUB]!!.Preference()
SettingsActivity2.allPrefs.map[NonSettingsPrefs.SAVE_LOG]!!.Preference()
}
}
@Preview
@Composable
private fun Preview() {
SettingsActivity2.allPrefs = AllPrefs(LocalContext.current)
Theme(true) {
Surface {
AboutScreen { }
}
}
}

View file

@ -0,0 +1,392 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R
import helium314.keyboard.latin.permissions.PermissionsUtil
import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.UserDictionaryListFragment
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.SpannableStringUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AllPrefs(context: Context) {
private val list = createPrefDefs(context)
val map: Map<String, PrefDef> = HashMap<String, PrefDef>(list.size).apply {
list.forEach {
if (put(it.key, it) != null)
throw IllegalArgumentException("key $it added twice")
}
}
// could be more elaborate, but should be good enough for a start
fun filter(searchTerm: String): List<PrefDef> {
val term = searchTerm.lowercase()
val results = mutableSetOf<PrefDef>()
list.forEach { if (it.title.lowercase().startsWith(term)) results.add(it) }
list.forEach { if (it.title.lowercase().split(' ').any { it.startsWith(term) }) results.add(it) }
list.forEach {
if (it.description?.lowercase()?.split(' ')?.any { it.startsWith(term) } == true)
results.add(it)
}
return results.toList()
}
}
class PrefDef(
context: Context,
val key: String,
@StringRes titleId: Int,
@StringRes descriptionId: Int? = null,
private val compose: @Composable (PrefDef) -> Unit
) {
val title = context.getString(titleId)
val description = descriptionId?.let { context.getString(it) }
@Composable
fun Preference() {
compose(this)
}
}
// this will be looooooong
private fun createPrefDefs(context: Context) = listOf(
// ---------------- correction ------------------
PrefDef(context, NonSettingsPrefs.EDIT_PERSONAL_DICTIONARY, R.string.edit_personal_dictionary) {
val ctx = LocalContext.current
Preference(
name = stringResource(R.string.edit_personal_dictionary),
onClick = { ctx.getActivity()?.switchTo(UserDictionaryListFragment()) },
)
},
PrefDef(context,
Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE,
R.string.prefs_block_potentially_offensive_title,
R.string.prefs_block_potentially_offensive_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_AUTO_CORRECTION,
R.string.autocorrect,
R.string.auto_correction_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_MORE_AUTO_CORRECTION,
R.string.more_autocorrect,
R.string.more_autocorrect_summary
) {
SwitchPreference(it, true) // todo: shouldn't it better be false?
},
PrefDef(context,
Settings.PREF_AUTOCORRECT_SHORTCUTS,
R.string.auto_correct_shortcuts,
R.string.auto_correct_shortcuts_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_AUTO_CORRECTION_CONFIDENCE,
R.string.auto_correction_confidence,
) { def ->
var showDialog by remember { mutableStateOf(false) }
// todo: arrays are arranged in a rather absurd way... this should be improved
val items = listOf(
stringResource(R.string.auto_correction_threshold_mode_modest) to "0",
stringResource(R.string.auto_correction_threshold_mode_aggressive) to "1",
stringResource(R.string.auto_correction_threshold_mode_very_aggressive) to "2",
)
val prefs = DeviceProtectedUtils.getSharedPreferences(LocalContext.current)
val selected = items.firstOrNull { it.second == prefs.getString(def.key, "0") }
Preference(
name = def.title,
description = selected?.first,
onClick = { showDialog = true }
)
if (showDialog) {
ListPickerDialog(
onDismissRequest = {showDialog = false },
items = items,
onItemSelected = {
if (it != selected)
prefs.edit().putString(def.key, it.second).apply()
},
selectedItem = selected,
title = { Text(def.title) },
getItemName = { it.first }
)
}
},
PrefDef(context,
Settings.PREF_AUTO_CAP,
R.string.auto_cap,
R.string.auto_cap_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD,
R.string.use_double_space_period,
R.string.use_double_space_period_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION,
R.string.autospace_after_punctuation,
R.string.autospace_after_punctuation_summary
) {
SwitchPreference(it, false)
},
PrefDef(context,
Settings.PREF_SHOW_SUGGESTIONS,
R.string.prefs_show_suggestions,
R.string.prefs_show_suggestions_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_ALWAYS_SHOW_SUGGESTIONS,
R.string.prefs_always_show_suggestions,
R.string.prefs_always_show_suggestions_summary
) {
SwitchPreference(it, false)
},
PrefDef(context,
Settings.PREF_KEY_USE_PERSONALIZED_DICTS,
R.string.use_personalized_dicts,
R.string.use_personalized_dicts_summary
) { prefDef ->
var showConfirmDialog by remember { mutableStateOf(false) }
SwitchPreference(
prefDef,
true,
allowCheckedChange = {
showConfirmDialog = !it
it
}
)
if (showConfirmDialog) {
val prefs = DeviceProtectedUtils.getSharedPreferences(LocalContext.current)
ConfirmationDialog(
onDismissRequest = { showConfirmDialog = false },
onConfirmed = {
prefs.edit().putBoolean(prefDef.key, false).apply()
},
text = { Text(stringResource(R.string.disable_personalized_dicts_message)) }
)
}
},
PrefDef(context,
Settings.PREF_BIGRAM_PREDICTIONS,
R.string.bigram_prediction,
R.string.bigram_prediction_summary
) {
SwitchPreference(it, true) { themeChanged = true }
},
PrefDef(context,
Settings.PREF_CENTER_SUGGESTION_TEXT_TO_ENTER,
R.string.center_suggestion_text_to_enter,
R.string.center_suggestion_text_to_enter_summary
) {
SwitchPreference(it, false)
},
PrefDef(context,
Settings.PREF_SUGGEST_CLIPBOARD_CONTENT,
R.string.suggest_clipboard_content,
R.string.suggest_clipboard_content_summary
) {
SwitchPreference(it, true)
},
PrefDef(context,
Settings.PREF_USE_CONTACTS,
R.string.use_contacts_dict,
R.string.use_contacts_dict_summary
) {
val activity = LocalContext.current.getActivity() ?: return@PrefDef
var granted by remember { mutableStateOf(PermissionsUtil.checkAllPermissionsGranted(activity, Manifest.permission.READ_CONTACTS)) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
granted = it
}
SwitchPreference(
it,
false,
allowCheckedChange = {
if (it && !granted) {
launcher.launch(Manifest.permission.READ_CONTACTS)
false
} else true
}
)
},
PrefDef(context,
Settings.PREF_ADD_TO_PERSONAL_DICTIONARY,
R.string.add_to_personal_dictionary,
R.string.add_to_personal_dictionary_summary
) {
SwitchPreference(it, false)
},
// ---------------- about ------------------
PrefDef(context, NonSettingsPrefs.APP, R.string.english_ime_name, R.string.app_slogan) {
Preference(
name = it.title,
description = it.description,
onClick = { },
icon = R.drawable.ic_launcher_foreground
)
},
PrefDef(context, NonSettingsPrefs.VERSION, R.string.version) {
var count by rememberSaveable { mutableIntStateOf(0) }
val ctx = LocalContext.current
val prefs = DeviceProtectedUtils.getSharedPreferences(ctx)
Preference(
name = it.title,
description = stringResource(R.string.version_text, BuildConfig.VERSION_NAME),
onClick = {
if (prefs.getBoolean(DebugSettings.PREF_SHOW_DEBUG_SETTINGS, false) || BuildConfig.DEBUG)
return@Preference
count++
if (count < 5) return@Preference
prefs.edit().putBoolean(DebugSettings.PREF_SHOW_DEBUG_SETTINGS, true).apply()
Toast.makeText(ctx, R.string.prefs_debug_settings_enabled, Toast.LENGTH_LONG).show()
},
icon = R.drawable.ic_settings_about_foreground
)
},
PrefDef(context, NonSettingsPrefs.LICENSE, R.string.license, R.string.gnu_gpl) {
val ctx = LocalContext.current
Preference(
name = it.title,
description = it.description,
onClick = {
val intent = Intent()
intent.data = "https://github.com/Helium314/HeliBoard/blob/main/LICENSE-GPL-3".toUri()
intent.action = Intent.ACTION_VIEW
ctx.startActivity(intent)
},
icon = R.drawable.ic_settings_about_license_foreground
)
},
PrefDef(context, NonSettingsPrefs.HIDDEN_FEATURES, R.string.hidden_features_title, R.string.hidden_features_summary) {
val ctx = LocalContext.current
Preference(
name = it.title,
description = it.description,
onClick = {
// Compose dialogs are in a rather sad state. They don't understand HTML, and don't scroll without customization.
// this should be re-done in compose, but... bah
val link = ("<a href=\"https://developer.android.com/reference/android/content/Context#createDeviceProtectedStorageContext()\">"
+ ctx.getString(R.string.hidden_features_text) + "</a>")
val message = ctx.getString(R.string.hidden_features_message, link)
val dialogMessage = SpannableStringUtils.fromHtml(message)
val builder = androidx.appcompat.app.AlertDialog.Builder(ctx)
.setIcon(R.drawable.ic_settings_about_hidden_features)
.setTitle(R.string.hidden_features_title)
.setMessage(dialogMessage)
.setPositiveButton(R.string.dialog_close, null)
.create()
builder.show()
(builder.findViewById<View>(android.R.id.message) as TextView).movementMethod = LinkMovementMethod.getInstance()
},
icon = R.drawable.ic_settings_about_hidden_features_foreground
)
},
PrefDef(context, NonSettingsPrefs.GITHUB, R.string.about_github_link) {
val ctx = LocalContext.current
Preference(
name = it.title,
description = it.description,
onClick = {
val intent = Intent()
intent.data = "https://github.com/Helium314/HeliBoard".toUri()
intent.action = Intent.ACTION_VIEW
ctx.startActivity(intent)
},
icon = R.drawable.ic_settings_about_github_foreground
)
},
PrefDef(context, NonSettingsPrefs.SAVE_LOG, R.string.save_log) {
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
ctx.getActivity()?.contentResolver?.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(Log.getLog().joinToString("\n")) }
}
}
}
Preference(
name = it.title,
description = it.description,
onClick = {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.putExtra(
Intent.EXTRA_TITLE,
ctx.getString(R.string.english_ime_name)
.replace(" ", "_") + "_log_${System.currentTimeMillis()}.txt"
)
.setType("text/plain")
launcher.launch(intent)
},
icon = R.drawable.ic_settings_about_log_foreground
)
},
)
// todo: move somewhere else
fun Context.getActivity(): ComponentActivity? {
val componentActivity = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
return componentActivity
}
object NonSettingsPrefs {
const val EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"
const val APP = "app"
const val VERSION = "version"
const val LICENSE = "license"
const val HIDDEN_FEATURES = "hidden_features"
const val GITHUB = "github"
const val SAVE_LOG = "save_log"
}
@JvmField
var themeChanged = false

View file

@ -0,0 +1,148 @@
package helium314.keyboard.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
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.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
// taken from StreetComplete
/** Slight specialization of an alert dialog: AlertDialog with OK and Cancel button. Both buttons
* call [onDismissRequest] and the OK button additionally calls [onConfirmed]. */
@Composable
fun ConfirmationDialog(
onDismissRequest: () -> Unit,
onConfirmed: () -> Unit,
modifier: Modifier = Modifier,
title: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit)? = null,
confirmButtonText: String = stringResource(android.R.string.ok),
cancelButtonText: String = stringResource(android.R.string.cancel),
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(backgroundColor),
properties: DialogProperties = DialogProperties(),
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = { onConfirmed(); onDismissRequest() }) { Text(confirmButtonText) }
},
modifier = modifier,
dismissButton = { TextButton(onClick = onDismissRequest) { Text(cancelButtonText) } },
title = title,
text = text,
shape = shape,
containerColor = backgroundColor,
textContentColor = contentColor,
properties = properties,
)
}
@Composable
fun <T> ListPickerDialog(
onDismissRequest: () -> Unit,
items: List<T>,
onItemSelected: (T) -> Unit,
modifier: Modifier = Modifier,
title: (@Composable () -> Unit)? = null,
selectedItem: T? = null,
getItemName: (@Composable (T) -> String) = { it.toString() },
width: Dp? = null,
height: Dp? = null,
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(backgroundColor),
properties: DialogProperties = DialogProperties()
) {
var selected by remember { mutableStateOf(selectedItem) }
val state = rememberLazyListState()
LaunchedEffect(selectedItem) {
val index = items.indexOf(selectedItem)
if (index != -1) state.scrollToItem(index, -state.layoutInfo.viewportSize.height / 3)
}
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
onClick = { onDismissRequest(); selected?.let { onItemSelected(it) } },
enabled = selected != null,
) { Text(stringResource(android.R.string.ok)) }
},
modifier = modifier,
dismissButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(android.R.string.cancel)) } },
title = title,
text = {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyLarge
) {
LazyColumn(state = state) {
items(items) { item ->
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { selected = item }
.padding(horizontal = 24.dp)
) {
Text(
text = getItemName(item),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
)
RadioButton(
selected = selected == item,
onClick = { selected = item }
)
}
}
}
}
},
shape = shape,
containerColor = backgroundColor,
textContentColor = contentColor,
properties = properties,
)
}
@Preview
@Composable
private fun PreviewListPickerDialog() {
val items = remember { (0..<5).toList() }
ListPickerDialog(
onDismissRequest = {},
items = items,
onItemSelected = {},
title = { Text("Select something") },
selectedItem = 2,
getItemName = { "Item $it" },
width = 260.dp
)
}

View file

@ -0,0 +1,118 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import android.app.Activity
import android.view.View
import android.widget.RelativeLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.fragment.app.commit
import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.AboutFragment
import helium314.keyboard.latin.settings.AdvancedSettingsFragment
import helium314.keyboard.latin.settings.AppearanceSettingsFragment
import helium314.keyboard.latin.settings.CorrectionSettingsFragment
import helium314.keyboard.latin.settings.GestureSettingsFragment
import helium314.keyboard.latin.settings.LanguageSettingsFragment
import helium314.keyboard.latin.settings.PreferencesSettingsFragment
import helium314.keyboard.latin.settings.ToolbarSettingsFragment
import helium314.keyboard.latin.utils.JniUtils
@Composable
fun MainSettingsScreen(
onClickAbout: () -> Unit,
onClickTextCorrection: () -> Unit,
onClickBack: () -> Unit,
) {
val ctx = LocalContext.current
SearchPrefScreen(
onClickBack = onClickBack,
title = stringResource(R.string.ime_settings),
) {
Preference(
name = stringResource(R.string.settings_screen_correction),
onClick = onClickTextCorrection,
icon = R.drawable.ic_settings_correction_foreground
) {
Icon(
painter = painterResource(R.drawable.ic_arrow_left),
modifier = Modifier.scale(-1f, 1f), // no rotate drawable allowed in compose
contentDescription = null
)
}
Preference(
name = stringResource(R.string.settings_screen_about),
onClick = onClickAbout,
icon = R.drawable.ic_settings_about_foreground
) {
Icon(
painter = painterResource(R.drawable.ic_arrow_left),
modifier = Modifier.scale(-1f, 1f),
contentDescription = null
)
}
PreferenceCategory(
title = "old screens"
) {
Preference(
name = stringResource(R.string.language_and_layouts_title),
onClick = { ctx.getActivity()?.switchTo(LanguageSettingsFragment()) }
)
Preference(
name = stringResource(R.string.settings_screen_preferences),
onClick = { ctx.getActivity()?.switchTo(PreferencesSettingsFragment()) }
)
Preference(
name = stringResource(R.string.settings_screen_appearance),
onClick = { ctx.getActivity()?.switchTo(AppearanceSettingsFragment()) }
)
Preference(
name = stringResource(R.string.settings_screen_toolbar),
onClick = { ctx.getActivity()?.switchTo(ToolbarSettingsFragment()) }
)
if (JniUtils.sHaveGestureLib)
Preference(
name = stringResource(R.string.settings_screen_gesture),
onClick = { ctx.getActivity()?.switchTo(GestureSettingsFragment()) }
)
Preference(
name = stringResource(R.string.settings_screen_correction),
onClick = { ctx.getActivity()?.switchTo(CorrectionSettingsFragment()) }
)
Preference(
name = stringResource(R.string.settings_screen_advanced),
onClick = { ctx.getActivity()?.switchTo(AdvancedSettingsFragment()) }
)
Preference(
name = stringResource(R.string.settings_screen_about),
onClick = { ctx.getActivity()?.switchTo(AboutFragment()) }
)
}
}
}
fun Activity.switchTo(fragment: androidx.fragment.app.Fragment) {
(this as AppCompatActivity).supportFragmentManager.commit {
findViewById<RelativeLayout>(R.id.settingsFragmentContainer).visibility = View.VISIBLE
replace(R.id.settingsFragmentContainer, fragment)
addToBackStack(null)
}
}
@Preview
@Composable
private fun PreviewScreen() {
Theme(true) {
Surface {
MainSettingsScreen({}, {}, {})
}
}
}

View file

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import helium314.keyboard.latin.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PrefScreen(
onClickBack: () -> Unit,
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Column(Modifier.fillMaxSize()) {
TopAppBar(
title = { Text(title) },
windowInsets = TopAppBarDefaults.windowInsets,
navigationIcon = {
IconButton(onClick = onClickBack) {
Icon(
painterResource(R.drawable.ic_arrow_left), // see SearchScreen
stringResource(R.string.spoken_description_action_previous)
)
}
},
)
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
))
) {
content()
}
}
}
}

View file

@ -0,0 +1,237 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
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.Hyphens
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.Log
// taken from StreetComplete (and a bit SCEE)
@Composable
fun PreferenceCategory(
title: String?,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
Column {
HorizontalDivider()
if (title != null) {
Text(
text = title,
modifier = modifier.padding(top = 12.dp, start = 16.dp, end = 8.dp, bottom = 8.dp),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
}
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
Column {
content()
}
}
}
}
@Composable
fun Preference(
name: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
description: String? = null,
@DrawableRes icon: Int? = null,
value: @Composable (RowScope.() -> Unit)? = null,
) {
Column(
modifier = modifier
.fillMaxWidth()
.clickable { onClick() }
.heightIn(min = 48.dp)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(
space = 0.dp,
alignment = Alignment.CenterVertically
)
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null)
Icon(painterResource(icon), name, modifier = Modifier.size(48.dp).padding(end = 8.dp))
Column(modifier = Modifier.weight(2 / 3f)) {
Text(text = name,)
if (description != null) {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyMedium,
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant
) {
Text(
text = description,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
if (value != null) {
CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.copy(
textAlign = TextAlign.End,
hyphens = Hyphens.Auto
),
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant
) {
Row(
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1 / 3f)
) { value() }
}
}
}
}
}
@Composable
fun SwitchPreference(
name: String,
modifier: Modifier = Modifier,
pref: String,
default: Boolean,
description: String? = null,
allowCheckedChange: (Boolean) -> Boolean = { true }, // true means ok, usually for showing some dialog
onCheckedChange: (Boolean) -> Unit = { },
) {
val ctx = LocalContext.current
val prefs = DeviceProtectedUtils.getSharedPreferences(ctx)
val b = (ctx.getActivity() as SettingsActivity2).prefChanged.collectAsState()
var value = prefs.getBoolean(pref, default)
if (b.value < 0)
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
fun switched(newValue: Boolean) {
if (!allowCheckedChange(newValue)) {
value = !newValue
return
}
value = newValue
prefs.edit().putBoolean(pref, newValue).apply()
onCheckedChange(newValue)
}
Preference(
name = name,
onClick = { switched(!value) },
modifier = modifier,
description = description
) {
Switch(
checked = value,
onCheckedChange = { switched(it) },
// switch is really ugly... how
// colors = SwitchDefaults.colors(uncheckedBorderColor = Color.Transparent)
)
}
}
@Composable
fun SwitchPreference(
def: PrefDef,
default: Boolean,
modifier: Modifier = Modifier,
allowCheckedChange: (Boolean) -> Boolean = { true },
onCheckedChange: (Boolean) -> Unit = { }
) {
SwitchPreference(
name = def.title,
description = def.description,
pref = def.key,
default = default,
modifier = modifier,
allowCheckedChange = allowCheckedChange,
onCheckedChange = onCheckedChange
)
}
@Preview
@Composable
private fun PreferencePreview() {
PreferenceCategory("Preference Category") {
Preference(
name = "Preference",
onClick = {},
)
Preference(
name = "Preference with icon",
onClick = {},
icon = R.drawable.ic_settings_about_foreground
)
Preference(
name = "Preference with icon and description",
description = "some text",
onClick = {},
icon = R.drawable.ic_settings_about_foreground
)
Preference(
name = "Preference with switch",
onClick = {}
) {
Switch(checked = true, onCheckedChange = {})
}
SwitchPreference(
name = "SwitchPreference",
pref = "none",
default = true
)
Preference(
name = "Preference",
onClick = {},
description = "A long description which may actually be several lines long, so it should wrap."
) {
Icon(painterResource(R.drawable.ic_arrow_left), null)
}
Preference(
name = "Long preference name that wraps",
onClick = {},
) {
Text("Long preference value")
}
Preference(
name = "Long preference name 2",
onClick = {},
description = "hello I am description"
) {
Text("Long preference value")
}
}
}

View file

@ -0,0 +1,168 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchPrefScreen(
onClickBack: () -> Unit,
title: String,
content: @Composable ColumnScope.() -> Unit
) {
var searchText by remember { mutableStateOf(TextFieldValue()) } // must be outside th column to work without messing up cursor position
Column(Modifier.fillMaxSize()) {
// rememberSaveable would be better, but does not work with TextFieldValue
// if we just store the string, the cursor is messed up
// hmm... no, sth else is messing up that thing, and I just didn't notice
var showSearch by remember { mutableStateOf(false) }
fun setShowSearch(value: Boolean) {
showSearch = value
if (!value) searchText = TextFieldValue()
}
BackHandler {
if (showSearch) setShowSearch(false)
else onClickBack()
}
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
//shadowElevation = TopAppBarDefaults.??
) {
Column {
TopAppBar(
title = { Text(title) },
windowInsets = TopAppBarDefaults.windowInsets,
navigationIcon = {
IconButton(onClick = {
if (showSearch) setShowSearch(false)
else onClickBack()
}) {
Icon(
painterResource(R.drawable.ic_arrow_left), // todo: "old" arrow icon existed, so must be somewhere in resources (maybe androidx?)
stringResource(R.string.spoken_description_action_previous)
)
}
},
actions = {
IconButton(onClick = { setShowSearch(!showSearch) }) { Icon(painterResource(R.drawable.sym_keyboard_search_lxx), stringResource(R.string.label_search_key)) }
},
//elevation = 0.dp
)
ExpandableSearchField(
expanded = showSearch,
onDismiss = { setShowSearch(false) },
search = searchText,
onSearchChange = { searchText = it },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
//colors = TextFieldDefaults.colors(
// textColor = MaterialTheme.colorScheme.onSurface,
// backgroundColor = MaterialTheme.colorScheme.surface
//)
)
}
}
if (searchText.text.isBlank())
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
))
) {
content()
}
}
else
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
val filteredPrefs = SettingsActivity2.allPrefs.filter(searchText.text)
LazyColumn(
//state = listState, // better not, remembering scroll state when changing search term feels wrong
// todo: scrolling should consider keyboard, but not working, from https://developer.android.com/develop/ui/compose/layouts/insets#ime-animations
//modifier = Modifier.imePadding().imeNestedScroll()
) {
items(filteredPrefs) {
it.Preference()
}
}
}
}
}
// from StreetComplete
/** Expandable text field that can be dismissed and requests focus when it is expanded */
@Composable
fun ExpandableSearchField(
expanded: Boolean,
onDismiss: () -> Unit,
search: TextFieldValue,
onSearchChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
colors: TextFieldColors = TextFieldDefaults.colors(),
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(expanded) {
if (expanded) focusRequester.requestFocus()
}
AnimatedVisibility(visible = expanded, modifier = Modifier.fillMaxWidth()) {
TextField(
value = search,
onValueChange = onSearchChange,
modifier = modifier.focusRequester(focusRequester),
leadingIcon = { Icon(painterResource(R.drawable.sym_keyboard_search_lxx), stringResource(R.string.label_search_key)) },
trailingIcon = { IconButton(onClick = {
if (search.text.isBlank()) onDismiss()
else onSearchChange(TextFieldValue())
}) { Icon(painterResource(R.drawable.ic_close), stringResource(android.R.string.cancel)) } },
singleLine = true,
colors = colors
)
}
}

View file

@ -0,0 +1,125 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.RelativeLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.Surface
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.isGone
import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import kotlinx.coroutines.flow.MutableStateFlow
// todo
// more pref screens, seekBarPref, reorderDialog, and other super-custom things
// consider IME insets when searching
// improve performance when loading screens with many settings (lazyColumn?)
// consider that stuff in composables can get called quite often on any changes -> use remember for things that are slow (maybe add test logging)
// later
// one single place for default values (in composables and settings)
// nice arrows (in top bar, and as next-screen indicator)
// animations when stuff (dis)appears
// LaunchedEffect, AnimatedVisibility
// remove PrefScreen if not used
// rename some classes
// split the preferences in allPrefs.createDefs into multiple files, this will get horribly long
// maybe have sub-lists in the pref screens using the settings?
// spdx headers everywhere
// changes to anything but the compose settings package should not be in the initial PR
// commit them separately if possible
// though some might be necessary
// toolbar key enabled state can be wrong
// go to correction settings, open search, toggle autocorrect toolbar key, and then toggle setting
// -> now toolbar key always has the wrong state
// color settings needs a color search
// more convenient access to prefs
// consider disabled settings & search
// don't show -> users confused
// show as disabled -> users confused
// show (but change will not do anything because another setting needs to be enabled first)
// -> users confused, but probably better than the 2 above
// maybe later
// bottom text field (though we have the search now anyway)
// remove navHost? but probably too useful to have...
// lazyColumn for prefs (or just in category?)
// should improve loading time for screens with many settings
// but needs a bit of work for probably not so much benefit
// adjust the debug settings thing, so that users can always find them in search but nowhere else? unless debug mode
// search only in current pref screen, except when in main?
// try getting rid of appcompat stuff (activity, dialogs, ...)
// preliminary results:
// looks ok (ugly M3 switches)
// performance
// time until app and screens are shown is clearly worse than previously (2-4x)
// gets much better when opening same screen again
// material3 is ~25% faster than material2
// debug is MUCH slower than release
// -> should be fine on reasonably recent phones (imo even still acceptable on S4 mini)
// apk size increase
// ca 900 kb with base + material2
// another 300 kb with navHost (and activity-compose, but not needed)
// another 300 kb when switching material2 to material3
// ca 150 kb reduction when removing androidx.preference
// -> too much, but still ok if we can get nicer preference stuff
class SettingsActivity2 : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private val prefs by lazy { DeviceProtectedUtils.getSharedPreferences(this) }
val prefChanged = MutableStateFlow(0) // simple counter, as the only relevant information is that something changed
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Settings.getInstance().current == null)
Settings.init(this)
// val cv = ComposeView(context = this)
allPrefs = AllPrefs(this)
// setContentView(cv) // todo: later, but for showing both old and new style settings, the layout is better
setContentView(R.layout.settings_activity)
supportFragmentManager.addOnBackStackChangedListener {
updateContainerVisibility()
}
// cv.setContent { // also later...
findViewById<ComposeView>(R.id.navHost).setContent {
Theme {
Surface {
SettingsNavHost(
onClickBack = {
if (supportFragmentManager.findFragmentById(R.id.settingsFragmentContainer) == null) // todo: remove after migration is complete
this.finish()
else supportFragmentManager.popBackStack() // todo: remove after migration is complete
}
)
}
}
}
}
private fun updateContainerVisibility() { // todo: remove after migration is complete
findViewById<RelativeLayout>(R.id.settingsFragmentContainer).isGone = supportFragmentManager.findFragmentById(R.id.settingsFragmentContainer) == null
}
override fun onStart() {
super.onStart()
prefs.registerOnSharedPreferenceChangeListener(this)
}
override fun onStop() {
prefs.unregisterOnSharedPreferenceChangeListener(this)
super.onStop()
}
companion object {
// public write so compose previews can show the pref screens
lateinit var allPrefs: AllPrefs
}
override fun onSharedPreferenceChanged(prefereces: SharedPreferences?, key: String?) {
prefChanged.value++
}
}

View file

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun SettingsNavHost(
onClickBack: () -> Unit,
startDestination: String? = null,
) {
val navController = rememberNavController()
val dir = if (LocalLayoutDirection.current == LayoutDirection.Ltr) 1 else -1
fun goBack() {
if (!navController.popBackStack()) onClickBack()
}
NavHost(
navController = navController,
startDestination = startDestination ?: SettingsDestination.Settings,
enterTransition = { slideInHorizontally(initialOffsetX = { +it * dir }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it * dir }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it * dir }) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { +it * dir }) }
) {
composable(SettingsDestination.Settings) {
MainSettingsScreen(
onClickAbout = { navController.navigate(SettingsDestination.About) },
onClickTextCorrection = { navController.navigate(SettingsDestination.TextCorrection) },
onClickBack = ::goBack,
)
}
composable(SettingsDestination.About) {
AboutScreen(
onClickBack = ::goBack
)
}
composable(SettingsDestination.TextCorrection) {
TextCorrectionScreen (
onClickBack = ::goBack
)
}
}
}
object SettingsDestination {
const val Settings = "settings"
const val About = "about"
const val TextCorrection = "text_correction"
}

View file

@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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 helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.Log
@Composable
fun TextCorrectionScreen(
onClickBack: () -> Unit,
) {
val prefs = DeviceProtectedUtils.getSharedPreferences(LocalContext.current)
val act = LocalContext.current.getActivity() as? SettingsActivity2
val b = act?.prefChanged!!.collectAsState()
if (b.value < 0)
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
val autocorrectEnabled = prefs.getBoolean(Settings.PREF_AUTO_CORRECTION, true)
val personalizedSuggestionsEnabled = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true)
val suggestionsEnabled = prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, true)
SearchPrefScreen(
onClickBack = onClickBack,
title = stringResource(R.string.settings_screen_correction),
) {
SettingsActivity2.allPrefs.map[NonSettingsPrefs.EDIT_PERSONAL_DICTIONARY]!!.Preference()
PreferenceCategory(stringResource(R.string.settings_category_correction)) {
SettingsActivity2.allPrefs.map[Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE]!!.Preference()
SettingsActivity2.allPrefs.map[Settings.PREF_AUTO_CORRECTION]!!.Preference()
AnimatedVisibility(visible = autocorrectEnabled, modifier = Modifier.fillMaxWidth()) {
SettingsActivity2.allPrefs.map[Settings.PREF_MORE_AUTO_CORRECTION]!!.Preference()
}
AnimatedVisibility(visible = autocorrectEnabled, modifier = Modifier.fillMaxWidth()) {
SettingsActivity2.allPrefs.map[Settings.PREF_AUTOCORRECT_SHORTCUTS]!!.Preference()
}
AnimatedVisibility(visible = autocorrectEnabled, modifier = Modifier.fillMaxWidth()) {
SettingsActivity2.allPrefs.map[Settings.PREF_AUTO_CORRECTION_CONFIDENCE]!!.Preference()
}
SettingsActivity2.allPrefs.map[Settings.PREF_AUTO_CAP]!!.Preference()
SettingsActivity2.allPrefs.map[Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD]!!.Preference()
SettingsActivity2.allPrefs.map[Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION]!!.Preference()
}
PreferenceCategory(stringResource(R.string.settings_category_suggestions)) {
SettingsActivity2.allPrefs.map[Settings.PREF_SHOW_SUGGESTIONS]!!.Preference()
AnimatedVisibility(visible = suggestionsEnabled, modifier = Modifier.fillMaxWidth()) {
SettingsActivity2.allPrefs.map[Settings.PREF_ALWAYS_SHOW_SUGGESTIONS]!!.Preference()
}
SettingsActivity2.allPrefs.map[Settings.PREF_KEY_USE_PERSONALIZED_DICTS]!!.Preference()
SettingsActivity2.allPrefs.map[Settings.PREF_BIGRAM_PREDICTIONS]!!.Preference()
AnimatedVisibility(visible = suggestionsEnabled, modifier = Modifier.fillMaxWidth()) {
SettingsActivity2.allPrefs.map[Settings.PREF_CENTER_SUGGESTION_TEXT_TO_ENTER]!!.Preference()
}
SettingsActivity2.allPrefs.map[Settings.PREF_SUGGEST_CLIPBOARD_CONTENT]!!.Preference()
SettingsActivity2.allPrefs.map[Settings.PREF_USE_CONTACTS]!!.Preference()
AnimatedVisibility(visible = personalizedSuggestionsEnabled, modifier = Modifier.fillMaxWidth()) {
SettingsActivity2.allPrefs.map[Settings.PREF_ADD_TO_PERSONAL_DICTIONARY]!!.Preference()
}
}
}
}
@Preview
@Composable
private fun PreferencePreview() {
SettingsActivity2.allPrefs = AllPrefs(LocalContext.current)
Theme(true) {
Surface {
TextCorrectionScreen { }
}
}
}

View file

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.font.DeviceFontFamilyName
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import helium314.keyboard.latin.R
@Composable
fun Theme(dark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val material3 = Typography()
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (dark) dynamicDarkColorScheme(LocalContext.current)
else dynamicLightColorScheme(LocalContext.current)
} else {
// todo: more colors
if (dark) darkColorScheme(
primary = colorResource(R.color.accent),
)
else lightColorScheme(
primary = colorResource(R.color.accent)
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(
headlineMedium = material3.headlineMedium.copy(fontWeight = FontWeight.Bold),
headlineSmall = material3.headlineSmall.copy(fontWeight = FontWeight.Bold),
titleLarge = material3.titleLarge.copy(
fontWeight = FontWeight.Bold,
fontFamily = FontFamily(Font(DeviceFontFamilyName("sans-serif-condensed"), FontWeight.Bold))
),
titleMedium = material3.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontFamily = FontFamily(Font(DeviceFontFamilyName("sans-serif-condensed"), FontWeight.Bold))
),
titleSmall = material3.titleSmall.copy(
fontWeight = FontWeight.Bold,
fontFamily = FontFamily(Font(DeviceFontFamilyName("sans-serif-condensed"), FontWeight.Bold))
)
),
//shapes = Shapes(),
content = content
)
}

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/navHost"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/settingsFragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/setup_background"
android:visibility="gone" >
</RelativeLayout>
</RelativeLayout>

View file

@ -6,7 +6,7 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="platformActivityTheme" parent="Theme.AppCompat.DayNight">
<style name="platformActivityTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:colorAccent">@color/accent</item>
<item name="colorAccent">@color/accent</item>