mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-21 06:39:09 +00:00
add new settings with search functionality (WIP)
This commit is contained in:
parent
fc12877795
commit
e1be94112e
21 changed files with 1539 additions and 11 deletions
|
@ -105,7 +105,6 @@ __Planned features and improvements:__
|
||||||
* [Bug fixes](https://github.com/Helium314/HeliBoard/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
* [Bug fixes](https://github.com/Helium314/HeliBoard/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||||
|
|
||||||
__What will _not_ be added:__
|
__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)
|
* Dictionaries for more languages (you can still download them)
|
||||||
* Anything that requires additional permissions, unless there is a _very_ good reason
|
* Anything that requires additional permissions, unless there is a _very_ good reason
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("plugin.serialization") version "2.0.21"
|
kotlin("plugin.serialization") version "2.0.21"
|
||||||
|
kotlin("plugin.compose") version "2.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -49,6 +50,7 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
|
@ -105,6 +107,14 @@ dependencies {
|
||||||
// kotlin
|
// kotlin
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
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
|
// color picker for user-defined colors
|
||||||
implementation("com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0")
|
implementation("com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0")
|
||||||
|
|
||||||
|
|
|
@ -74,10 +74,9 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".settings.SettingsActivity"
|
<activity android:name="helium314.keyboard.settings.SettingsActivity2"
|
||||||
android:theme="@style/platformActivityTheme"
|
android:theme="@style/platformActivityTheme"
|
||||||
android:label="@string/ime_settings"
|
android:label="@string/ime_settings"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
@ -89,6 +89,7 @@ import helium314.keyboard.latin.utils.StatsUtilsManager;
|
||||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
|
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
|
||||||
import helium314.keyboard.latin.utils.SubtypeSettingsKt;
|
import helium314.keyboard.latin.utils.SubtypeSettingsKt;
|
||||||
import helium314.keyboard.latin.utils.ViewLayoutUtils;
|
import helium314.keyboard.latin.utils.ViewLayoutUtils;
|
||||||
|
import helium314.keyboard.settings.AllPrefsKt;
|
||||||
import kotlin.collections.CollectionsKt;
|
import kotlin.collections.CollectionsKt;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
|
@ -879,6 +880,8 @@ public class LatinIME extends InputMethodService implements
|
||||||
void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
|
void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
|
||||||
super.onStartInput(editorInfo, restarting);
|
super.onStartInput(editorInfo, restarting);
|
||||||
|
|
||||||
|
reloadIfNecessary();
|
||||||
|
|
||||||
final List<Locale> hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo);
|
final List<Locale> hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo);
|
||||||
if (hintLocales == null) {
|
if (hintLocales == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -1976,4 +1979,12 @@ public class LatinIME extends InputMethodService implements
|
||||||
// deallocateMemory always called on hiding, and should not be called when showing
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
package helium314.keyboard.latin
|
package helium314.keyboard.latin
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -389,8 +389,13 @@ public class SettingsValues {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
|
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
|
||||||
return prefs.getBoolean(Settings.PREF_USE_CONTACTS, false)
|
final boolean setting = prefs.getBoolean(Settings.PREF_USE_CONTACTS, false);
|
||||||
&& PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS);
|
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() {
|
public String dump() {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import helium314.keyboard.latin.utils.JniUtils;
|
||||||
import helium314.keyboard.latin.utils.LeakGuardHandlerWrapper;
|
import helium314.keyboard.latin.utils.LeakGuardHandlerWrapper;
|
||||||
import helium314.keyboard.latin.utils.ResourceUtils;
|
import helium314.keyboard.latin.utils.ResourceUtils;
|
||||||
import helium314.keyboard.latin.utils.UncachedInputMethodManagerUtils;
|
import helium314.keyboard.latin.utils.UncachedInputMethodManagerUtils;
|
||||||
|
import helium314.keyboard.settings.SettingsActivity2;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
@ -104,10 +105,9 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
final ActionBar actionBar = getSupportActionBar();
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
if (actionBar == null) {
|
if (actionBar != null) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
actionBar.hide();
|
actionBar.hide();
|
||||||
|
}
|
||||||
getWindow().setStatusBarColor(getResources().getColor(R.color.setup_background));
|
getWindow().setStatusBarColor(getResources().getColor(R.color.setup_background));
|
||||||
ActivityThemeUtils.setActivityTheme(this);
|
ActivityThemeUtils.setActivityTheme(this);
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
|
||||||
0 /* finishedInstruction */, R.drawable.sym_keyboard_language_switch,
|
0 /* finishedInstruction */, R.drawable.sym_keyboard_language_switch,
|
||||||
R.string.setup_step3_action);
|
R.string.setup_step3_action);
|
||||||
step3.setAction(() -> {
|
step3.setAction(() -> {
|
||||||
final Intent intent = new Intent(getApplicationContext(), SettingsActivity.class);
|
final Intent intent = new Intent(getApplicationContext(), SettingsActivity2.class);
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
|
@ -224,7 +224,7 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
|
||||||
|
|
||||||
private void invokeSettingsOfThisIme() {
|
private void invokeSettingsOfThisIme() {
|
||||||
final Intent intent = new Intent();
|
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.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
||||||
|
|
|
@ -27,6 +27,10 @@ public final class DeviceProtectedUtils {
|
||||||
return prefs;
|
return prefs;
|
||||||
}
|
}
|
||||||
Context deviceProtectedContext = getDeviceProtectedContext(context);
|
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);
|
prefs = PreferenceManager.getDefaultSharedPreferences(deviceProtectedContext);
|
||||||
if (prefs.getAll().isEmpty()) {
|
if (prefs.getAll().isEmpty()) {
|
||||||
Log.i(TAG, "Device encrypted storage is empty, copying values from credential encrypted storage");
|
Log.i(TAG, "Device encrypted storage is empty, copying values from credential encrypted storage");
|
||||||
|
|
37
app/src/main/java/helium314/keyboard/settings/AboutScreen.kt
Normal file
37
app/src/main/java/helium314/keyboard/settings/AboutScreen.kt
Normal 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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
392
app/src/main/java/helium314/keyboard/settings/AllPrefs.kt
Normal file
392
app/src/main/java/helium314/keyboard/settings/AllPrefs.kt
Normal 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
|
148
app/src/main/java/helium314/keyboard/settings/Dialogs.kt
Normal file
148
app/src/main/java/helium314/keyboard/settings/Dialogs.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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({}, {}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
app/src/main/java/helium314/keyboard/settings/PrefScreen.kt
Normal file
62
app/src/main/java/helium314/keyboard/settings/PrefScreen.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
237
app/src/main/java/helium314/keyboard/settings/Preference.kt
Normal file
237
app/src/main/java/helium314/keyboard/settings/Preference.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
168
app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
Normal file
168
app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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++
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
app/src/main/java/helium314/keyboard/settings/Theme.kt
Normal file
57
app/src/main/java/helium314/keyboard/settings/Theme.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
19
app/src/main/res/layout/settings_activity.xml
Normal file
19
app/src/main/res/layout/settings_activity.xml
Normal 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>
|
|
@ -6,7 +6,7 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<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="android:colorAccent">@color/accent</item>
|
||||||
<item name="colorAccent">@color/accent</item>
|
<item name="colorAccent">@color/accent</item>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue