diff --git a/README.md b/README.md index 69935b757..ae62abc00 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d6bc1bec..a8941e866 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f0cee5671..8dc0b0a48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -74,10 +74,9 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only - diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java index 592af48d7..fb57f98fb 100644 --- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java +++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java @@ -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 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; + } + } } diff --git a/app/src/main/java/helium314/keyboard/latin/Settings.kt b/app/src/main/java/helium314/keyboard/latin/Settings.kt index 4d5ea2977..278f4d8bc 100644 --- a/app/src/main/java/helium314/keyboard/latin/Settings.kt +++ b/app/src/main/java/helium314/keyboard/latin/Settings.kt @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only package helium314.keyboard.latin import android.content.Context diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java index eae7b375f..3b6c12d1d 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -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() { diff --git a/app/src/main/java/helium314/keyboard/latin/setup/SetupWizardActivity.java b/app/src/main/java/helium314/keyboard/latin/setup/SetupWizardActivity.java index 6bab89934..0b0829922 100644 --- a/app/src/main/java/helium314/keyboard/latin/setup/SetupWizardActivity.java +++ b/app/src/main/java/helium314/keyboard/latin/setup/SetupWizardActivity.java @@ -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, diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DeviceProtectedUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/DeviceProtectedUtils.java index 81b302fd7..8a3da20c7 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/DeviceProtectedUtils.java +++ b/app/src/main/java/helium314/keyboard/latin/utils/DeviceProtectedUtils.java @@ -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"); diff --git a/app/src/main/java/helium314/keyboard/settings/AboutScreen.kt b/app/src/main/java/helium314/keyboard/settings/AboutScreen.kt new file mode 100644 index 000000000..203e44a18 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/AboutScreen.kt @@ -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 { } + } + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/AllPrefs.kt b/app/src/main/java/helium314/keyboard/settings/AllPrefs.kt new file mode 100644 index 000000000..0f336a2ec --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/AllPrefs.kt @@ -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 = HashMap(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 { + val term = searchTerm.lowercase() + val results = mutableSetOf() + 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 = ("" + + ctx.getString(R.string.hidden_features_text) + "") + 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(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 diff --git a/app/src/main/java/helium314/keyboard/settings/Dialogs.kt b/app/src/main/java/helium314/keyboard/settings/Dialogs.kt new file mode 100644 index 000000000..a8ff52a98 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/Dialogs.kt @@ -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 ListPickerDialog( + onDismissRequest: () -> Unit, + items: List, + 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 + ) +} diff --git a/app/src/main/java/helium314/keyboard/settings/MainSettingsScreen.kt b/app/src/main/java/helium314/keyboard/settings/MainSettingsScreen.kt new file mode 100644 index 000000000..5cb2ed131 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/MainSettingsScreen.kt @@ -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(R.id.settingsFragmentContainer).visibility = View.VISIBLE + replace(R.id.settingsFragmentContainer, fragment) + addToBackStack(null) + } +} + +@Preview +@Composable +private fun PreviewScreen() { + Theme(true) { + Surface { + MainSettingsScreen({}, {}, {}) + } + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/PrefScreen.kt b/app/src/main/java/helium314/keyboard/settings/PrefScreen.kt new file mode 100644 index 000000000..5be2f7ba5 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/PrefScreen.kt @@ -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() + } + } + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/Preference.kt b/app/src/main/java/helium314/keyboard/settings/Preference.kt new file mode 100644 index 000000000..69a4cdebf --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/Preference.kt @@ -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") + } + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt new file mode 100644 index 000000000..28facfb47 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt new file mode 100644 index 000000000..ac51fdaac --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt @@ -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(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(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++ + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt b/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt new file mode 100644 index 000000000..a2254c94a --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt @@ -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" +} diff --git a/app/src/main/java/helium314/keyboard/settings/TextCorrectionScreen.kt b/app/src/main/java/helium314/keyboard/settings/TextCorrectionScreen.kt new file mode 100644 index 000000000..42c48a67c --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/TextCorrectionScreen.kt @@ -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 { } + } + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/Theme.kt b/app/src/main/java/helium314/keyboard/settings/Theme.kt new file mode 100644 index 000000000..d261e2b8b --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/Theme.kt @@ -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 + ) +} diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 000000000..b689a448d --- /dev/null +++ b/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/values/platform-theme.xml b/app/src/main/res/values/platform-theme.xml index ed9fc5af6..432d20715 100644 --- a/app/src/main/res/values/platform-theme.xml +++ b/app/src/main/res/values/platform-theme.xml @@ -6,7 +6,7 @@ --> -