mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-20 06:09:09 +00:00
Settings upgrade (#1325)
Re-implement most of the settings in compose, searchable Languages & Layouts, Colors, and Personal Dictionary still missing, will be done later
This commit is contained in:
parent
679754bb2d
commit
e845e38e42
52 changed files with 4708 additions and 24 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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,16 @@ 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")
|
||||
implementation("sh.calvin.reorderable:reorderable:2.4.2") // for easier re-ordering
|
||||
implementation("com.github.skydoves:colorpicker-compose:1.1.2") // for user-defined colors
|
||||
|
||||
// color picker for user-defined colors
|
||||
implementation("com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0")
|
||||
|
||||
|
|
|
@ -74,10 +74,10 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".settings.SettingsActivity"
|
||||
<activity android:name="helium314.keyboard.settings.SettingsActivity"
|
||||
android:theme="@style/platformActivityTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/ime_settings"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
@ -113,6 +113,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
}
|
||||
|
||||
public void forceUpdateKeyboardTheme(@NonNull Context displayContext) {
|
||||
Settings settings = Settings.getInstance();
|
||||
settings.loadSettings(displayContext, settings.getCurrent().mLocale, settings.getCurrent().mInputAttributes);
|
||||
mLatinIME.setInputView(onCreateInputView(displayContext, mIsHardwareAcceleratedDrawingEnabled));
|
||||
}
|
||||
|
||||
|
@ -489,8 +491,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
// Reload the entire keyboard set with the same parameters, and switch to the previous layout
|
||||
boolean wasEmoji = isShowingEmojiPalettes();
|
||||
boolean wasClipboard = isShowingClipboardHistory();
|
||||
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), settings.getCurrent(),
|
||||
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
|
||||
reloadKeyboard();
|
||||
if (wasEmoji)
|
||||
setEmojiKeyboard();
|
||||
else if (wasClipboard) {
|
||||
|
@ -511,8 +512,13 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
!settings.getCurrent().mIsSplitKeyboardEnabled,
|
||||
mCurrentOrientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
);
|
||||
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), settings.getCurrent(),
|
||||
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
|
||||
reloadKeyboard();
|
||||
}
|
||||
|
||||
public void reloadKeyboard() {
|
||||
if (mCurrentInputView != null)
|
||||
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getInstance().getCurrent(),
|
||||
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -73,7 +73,6 @@ import helium314.keyboard.latin.inputlogic.InputLogic;
|
|||
import helium314.keyboard.latin.permissions.PermissionsManager;
|
||||
import helium314.keyboard.latin.personalization.PersonalizationHelper;
|
||||
import helium314.keyboard.latin.settings.Settings;
|
||||
import helium314.keyboard.latin.settings.SettingsActivity;
|
||||
import helium314.keyboard.latin.settings.SettingsValues;
|
||||
import helium314.keyboard.latin.suggestions.SuggestionStripView;
|
||||
import helium314.keyboard.latin.suggestions.SuggestionStripViewAccessor;
|
||||
|
@ -89,6 +88,8 @@ 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.SettingsActivity;
|
||||
import helium314.keyboard.settings.SettingsActivityKt;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
|
@ -900,6 +901,8 @@ public class LatinIME extends InputMethodService implements
|
|||
void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
|
||||
super.onStartInputView(editorInfo, restarting);
|
||||
|
||||
reloadIfNecessary();
|
||||
|
||||
mDictionaryFacilitator.onStartInput();
|
||||
// Switch to the null consumer to handle cases leading to early exit below, for which we
|
||||
// also wouldn't be consuming gesture data.
|
||||
|
@ -1976,4 +1979,13 @@ 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 (SettingsActivityKt.keyboardNeedsReload) {
|
||||
KeyboardLayoutSet.onKeyboardThemeChanged();
|
||||
mKeyboardSwitcher.forceUpdateKeyboardTheme(mDisplayContext);
|
||||
SettingsActivityKt.keyboardNeedsReload = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.latin
|
||||
|
||||
import android.content.Context
|
||||
|
|
|
@ -30,7 +30,7 @@ import helium314.keyboard.latin.R;
|
|||
public final class DebugSettingsFragment extends SubScreenFragment
|
||||
implements Preference.OnPreferenceClickListener {
|
||||
private static final String PREF_KEY_DUMP_DICTS = "dump_dictionaries";
|
||||
private static final String PREF_KEY_DUMP_DICT_PREFIX = "dump_dictionaries";
|
||||
public static final String PREF_KEY_DUMP_DICT_PREFIX = "dump_dictionaries";
|
||||
|
||||
private boolean mServiceNeedsRestart = false;
|
||||
private TwoStatePreference mDebugMode;
|
||||
|
|
|
@ -21,7 +21,7 @@ import androidx.appcompat.app.ActionBar;
|
|||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
public final class SettingsActivity extends AppCompatActivity
|
||||
public final class OldSettingsActivity extends AppCompatActivity
|
||||
implements ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
|
||||
|
|
@ -400,8 +400,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() {
|
||||
|
|
|
@ -26,12 +26,12 @@ import androidx.core.content.res.ResourcesCompat;
|
|||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import helium314.keyboard.latin.R;
|
||||
import helium314.keyboard.latin.settings.SettingsActivity;
|
||||
import helium314.keyboard.latin.utils.ActivityThemeUtils;
|
||||
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.SettingsActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
@ -104,10 +104,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);
|
||||
|
||||
|
@ -227,8 +226,8 @@ public final class SetupWizardActivity extends AppCompatActivity implements View
|
|||
intent.setClass(this, SettingsActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY,
|
||||
SettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON);
|
||||
// intent.putExtra(OldSettingsActivity.EXTRA_ENTRY_KEY,
|
||||
// OldSettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ fun loadCustomLayout(layoutContent: String, layoutName: String, languageTag: Str
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun checkLayout(layoutContent: String, context: Context): Boolean {
|
||||
fun checkLayout(layoutContent: String, context: Context): Boolean {
|
||||
val params = KeyboardParams()
|
||||
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
|
||||
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
package helium314.keyboard.latin.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.SharedPreferences
|
||||
import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.commit
|
||||
import helium314.keyboard.latin.R
|
||||
|
||||
// generic extension functions
|
||||
|
||||
|
@ -13,9 +22,9 @@ inline fun <T> Iterable<T>.sumOf(selector: (T) -> Float): Float {
|
|||
return sum
|
||||
}
|
||||
|
||||
fun CharSequence.getStringResourceOrName(prefix: String, context: Context): CharSequence {
|
||||
fun CharSequence.getStringResourceOrName(prefix: String, context: Context): String {
|
||||
val resId = context.resources.getIdentifier(prefix + this, "string", context.packageName)
|
||||
return if (resId == 0) this else context.getString(resId)
|
||||
return if (resId == 0) this.toString() else context.getString(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,3 +63,23 @@ fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, with: (T) -> T) {
|
|||
val i = indexOfFirst(predicate)
|
||||
if (i >= 0) this[i] = with(this[i])
|
||||
}
|
||||
|
||||
fun Context.getActivity(): ComponentActivity? {
|
||||
val componentActivity = when (this) {
|
||||
is ComponentActivity -> this
|
||||
is ContextWrapper -> baseContext.getActivity()
|
||||
else -> null
|
||||
}
|
||||
return componentActivity
|
||||
}
|
||||
|
||||
// todo: should not be necessary after full pref switch to compose
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.prefs(): SharedPreferences = DeviceProtectedUtils.getSharedPreferences(this)
|
||||
|
|
|
@ -322,12 +322,12 @@ fun readCustomLongpressCodes(prefs: SharedPreferences) = prefs.getString(Setting
|
|||
it.substringBefore(",") to code
|
||||
}
|
||||
|
||||
private fun writeCustomKeyCodes(prefs: SharedPreferences, codes: Map<String, Int?>) {
|
||||
fun writeCustomKeyCodes(prefs: SharedPreferences, codes: Map<String, Int?>) {
|
||||
val string = codes.mapNotNull { entry -> entry.value?.let { "${entry.key},$it" } }.joinToString(";")
|
||||
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, string).apply()
|
||||
}
|
||||
|
||||
private fun writeCustomLongpressCodes(prefs: SharedPreferences, codes: Map<String, Int?>) {
|
||||
fun writeCustomLongpressCodes(prefs: SharedPreferences, codes: Map<String, Int?>) {
|
||||
val string = codes.mapNotNull { entry -> entry.value?.let { "${entry.key},$it" } }.joinToString(";")
|
||||
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES, string).apply()
|
||||
}
|
||||
|
|
212
app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
Normal file
212
app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
Normal file
|
@ -0,0 +1,212 @@
|
|||
// 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.saveable.rememberSaveable
|
||||
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
|
||||
import helium314.keyboard.settings.preferences.PreferenceCategory
|
||||
|
||||
@Composable
|
||||
fun SearchSettingsScreen(
|
||||
onClickBack: () -> Unit,
|
||||
title: String,
|
||||
settings: List<Any?>,
|
||||
content: @Composable (ColumnScope.() -> Unit)? = null // overrides settings if not null
|
||||
) {
|
||||
SearchScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = title,
|
||||
content = {
|
||||
if (content != null) content()
|
||||
else {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
settings.forEach {
|
||||
if (it is Int) {
|
||||
PreferenceCategory(stringResource(it))
|
||||
} else {
|
||||
// this only animates appearing prefs
|
||||
// a solution would be using a list(visible to key)
|
||||
AnimatedVisibility(visible = it != null) {
|
||||
if (it != null)
|
||||
SettingsActivity.settingsContainer[it]?.Preference()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// lazyColumn has janky scroll for a while (not sure why compose gets smoother after a while)
|
||||
// maybe related to unnecessary recompositions? but even for just displaying text it's there
|
||||
// didn't manage to improve things with @Immutable list wrapper and other lazy list hints
|
||||
// so for now: just use "normal" Column
|
||||
// even though it takes up to ~50% longer to load it's much better UX
|
||||
// and the missing appear animations could be added
|
||||
// LazyColumn {
|
||||
// items(prefs.filterNotNull(), key = { it }) {
|
||||
// Box(Modifier.animateItem()) {
|
||||
// if (it is Int)
|
||||
// PreferenceCategory(stringResource(it))
|
||||
// else
|
||||
// SettingsActivity.settingsContainer[it]!!.Preference()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
},
|
||||
filteredItems = { SettingsActivity.settingsContainer.filter(it) },
|
||||
itemContent = { it.Preference() }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun <T: Any> SearchScreen(
|
||||
onClickBack: () -> Unit,
|
||||
title: String,
|
||||
filteredItems: (String) -> List<T>,
|
||||
itemContent: @Composable (T) -> Unit,
|
||||
content: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
) {
|
||||
var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun setShowSearch(value: Boolean) {
|
||||
showSearch = value
|
||||
if (!value) searchText = TextFieldValue()
|
||||
}
|
||||
BackHandler {
|
||||
if (showSearch) setShowSearch(false)
|
||||
else onClickBack()
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text(title) },
|
||||
windowInsets = TopAppBarDefaults.windowInsets,
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
if (showSearch) setShowSearch(false)
|
||||
else onClickBack()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.baseline_arrow_back_24),
|
||||
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)) }
|
||||
},
|
||||
)
|
||||
ExpandableSearchField(
|
||||
expanded = showSearch,
|
||||
onDismiss = { setShowSearch(false) },
|
||||
search = searchText,
|
||||
onSearchChange = { searchText = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
||||
if (searchText.text.isBlank() && content != null) {
|
||||
Column(
|
||||
Modifier
|
||||
.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
||||
)
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
} else {
|
||||
val items = filteredItems(searchText.text)
|
||||
LazyColumn {
|
||||
items(items) {
|
||||
itemContent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,83 @@
|
|||
// 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.prefs
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
// todo: with compose, app startup is slower and UI needs some "warmup" time to be snappy
|
||||
// maybe baseline profiles help?
|
||||
// https://developer.android.com/codelabs/android-baseline-profiles-improve
|
||||
// https://developer.android.com/codelabs/jetpack-compose-performance#2
|
||||
// https://developer.android.com/topic/performance/baselineprofiles/overview
|
||||
class SettingsActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private val prefs by lazy { this.prefs() }
|
||||
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)
|
||||
|
||||
settingsContainer = SettingsContainer(this)
|
||||
|
||||
// todo: when removing old settings completely, remove settings_activity.xml and supportFragmentManager stuff
|
||||
// val cv = ComposeView(context = this)
|
||||
// setContentView(cv)
|
||||
setContentView(R.layout.settings_activity)
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
updateContainerVisibility()
|
||||
}
|
||||
// cv.setContent { // todo: when removing old settings
|
||||
findViewById<ComposeView>(R.id.navHost).setContent {
|
||||
Theme {
|
||||
Surface {
|
||||
SettingsNavHost(
|
||||
onClickBack = {
|
||||
// this.finish() // todo: when removing old settings
|
||||
if (supportFragmentManager.findFragmentById(R.id.settingsFragmentContainer) == null)
|
||||
this.finish()
|
||||
else supportFragmentManager.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContainerVisibility() { // todo: remove when removing old settings
|
||||
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 screens
|
||||
// having it in a companion object is not ideal as it will stay in memory even after settings are closed
|
||||
// but it's small enough to not care
|
||||
lateinit var settingsContainer: SettingsContainer
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefereces: SharedPreferences?, key: String?) {
|
||||
prefChanged.value++
|
||||
}
|
||||
}
|
||||
|
||||
@JvmField
|
||||
var keyboardNeedsReload = false
|
|
@ -0,0 +1,86 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import helium314.keyboard.settings.screens.createAboutSettings
|
||||
import helium314.keyboard.settings.screens.createAdvancedSettings
|
||||
import helium314.keyboard.settings.screens.createAppearanceSettings
|
||||
import helium314.keyboard.settings.screens.createCorrectionSettings
|
||||
import helium314.keyboard.settings.screens.createGestureTypingSettings
|
||||
import helium314.keyboard.settings.screens.createPreferencesSettings
|
||||
import helium314.keyboard.settings.screens.createToolbarSettings
|
||||
|
||||
class SettingsContainer(context: Context) {
|
||||
private val list = createSettings(context)
|
||||
private val map: Map<String, Setting> = HashMap<String, Setting>(list.size).apply {
|
||||
list.forEach {
|
||||
if (put(it.key, it) != null)
|
||||
throw IllegalArgumentException("key $it added twice")
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(key: Any): Setting? = map[key]
|
||||
|
||||
// filtering could be more elaborate, but should be good enough for a start
|
||||
// always have all settings in search, because:
|
||||
// don't show disabled settings -> users confused
|
||||
// show as disabled (i.e. no interaction possible) -> users confused
|
||||
// show, but change will not do anything because another setting needs to be enabled first -> probably best
|
||||
fun filter(searchTerm: String): List<Setting> {
|
||||
val term = searchTerm.lowercase()
|
||||
val results = mutableSetOf<Setting>()
|
||||
list.forEach { setting -> if (setting.title.lowercase().startsWith(term)) results.add(setting) }
|
||||
list.forEach { setting -> if (setting.title.lowercase().split(' ').any { it.startsWith(term) }) results.add(setting) }
|
||||
list.forEach { setting ->
|
||||
if (setting.description?.lowercase()?.split(' ')?.any { it.startsWith(term) } == true)
|
||||
results.add(setting)
|
||||
}
|
||||
return results.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class Setting(
|
||||
context: Context,
|
||||
val key: String,
|
||||
@StringRes titleId: Int,
|
||||
@StringRes descriptionId: Int? = null,
|
||||
private val content: @Composable (Setting) -> Unit
|
||||
) {
|
||||
val title = context.getString(titleId)
|
||||
val description = descriptionId?.let { context.getString(it) }
|
||||
|
||||
@Composable
|
||||
fun Preference() {
|
||||
content(this)
|
||||
}
|
||||
}
|
||||
|
||||
// intentionally not putting individual debug settings in here so user knows the context
|
||||
private fun createSettings(context: Context) = createAboutSettings(context) +
|
||||
createCorrectionSettings(context) + createPreferencesSettings(context) + createToolbarSettings(context) +
|
||||
createGestureTypingSettings(context) + createAdvancedSettings(context) + createAppearanceSettings(context)
|
||||
|
||||
object SettingsWithoutKey {
|
||||
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"
|
||||
const val CUSTOM_KEY_CODES = "customize_key_codes"
|
||||
const val CUSTOM_SYMBOLS_NUMBER_LAYOUTS = "custom_symbols_number_layouts"
|
||||
const val CUSTOM_FUNCTIONAL_LAYOUTS = "custom_functional_key_layouts"
|
||||
const val BACKUP_RESTORE = "backup_restore"
|
||||
const val DEBUG_SETTINGS = "screen_debug"
|
||||
const val LOAD_GESTURE_LIB = "load_gesture_library"
|
||||
const val ADJUST_COLORS = "adjust_colors"
|
||||
const val ADJUST_COLORS_NIGHT = "adjust_colors_night"
|
||||
const val BACKGROUND_IMAGE = "background_image"
|
||||
const val BACKGROUND_IMAGE_LANDSCAPE = "background_image_landscape"
|
||||
const val CUSTOM_FONT = "custom_font"
|
||||
}
|
137
app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt
Normal file
137
app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt
Normal file
|
@ -0,0 +1,137 @@
|
|||
// 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.runtime.collectAsState
|
||||
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
|
||||
import helium314.keyboard.settings.screens.AboutScreen
|
||||
import helium314.keyboard.settings.screens.AdvancedSettingsScreen
|
||||
import helium314.keyboard.settings.screens.AppearanceScreen
|
||||
import helium314.keyboard.settings.screens.DebugScreen
|
||||
import helium314.keyboard.settings.screens.GestureTypingScreen
|
||||
import helium314.keyboard.settings.screens.MainSettingsScreen
|
||||
import helium314.keyboard.settings.screens.PreferencesScreen
|
||||
import helium314.keyboard.settings.screens.TextCorrectionScreen
|
||||
import helium314.keyboard.settings.screens.ToolbarScreen
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SettingsNavHost(
|
||||
onClickBack: () -> Unit,
|
||||
startDestination: String? = null,
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val dir = if (LocalLayoutDirection.current == LayoutDirection.Ltr) 1 else -1
|
||||
val target = SettingsDestination.navTarget.collectAsState()
|
||||
|
||||
fun goBack() {
|
||||
if (!navController.popBackStack()) onClickBack()
|
||||
}
|
||||
if (target.value != SettingsDestination.Settings)
|
||||
navController.navigate(route = target.value)
|
||||
|
||||
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) },
|
||||
onClickPreferences = { navController.navigate(SettingsDestination.Preferences) },
|
||||
onClickToolbar = { navController.navigate(SettingsDestination.Toolbar) },
|
||||
onClickGestureTyping = { navController.navigate(SettingsDestination.GestureTyping) },
|
||||
onClickAdvanced = { navController.navigate(SettingsDestination.Advanced) },
|
||||
onClickAppearance = { navController.navigate(SettingsDestination.Appearance) },
|
||||
onClickBack = ::goBack,
|
||||
)
|
||||
}
|
||||
composable(SettingsDestination.About) {
|
||||
AboutScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.TextCorrection) {
|
||||
TextCorrectionScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.Preferences) {
|
||||
PreferencesScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.Toolbar) {
|
||||
ToolbarScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.GestureTyping) {
|
||||
GestureTypingScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.Advanced) {
|
||||
AdvancedSettingsScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.Debug) {
|
||||
DebugScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.Appearance) {
|
||||
AppearanceScreen(onClickBack = ::goBack)
|
||||
}
|
||||
composable(SettingsDestination.PersonalDictionary) {
|
||||
// PersonalDictionarySettingsScreen(
|
||||
// onClickBack = ::goBack
|
||||
// )
|
||||
}
|
||||
composable(SettingsDestination.Languages) {
|
||||
// LanguagesSettingsScreen(
|
||||
// onClickBack = ::goBack
|
||||
// )
|
||||
}
|
||||
composable(SettingsDestination.Colors) {
|
||||
// ColorsScreen(
|
||||
// night = false,
|
||||
// onClickBack = ::goBack
|
||||
// )
|
||||
}
|
||||
composable(SettingsDestination.ColorsNight) {
|
||||
// ColorsScreen(
|
||||
// night = true,
|
||||
// onClickBack = ::goBack
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SettingsDestination {
|
||||
const val Settings = "settings"
|
||||
const val About = "about"
|
||||
const val TextCorrection = "text_correction"
|
||||
const val Preferences = "preferences"
|
||||
const val Toolbar = "toolbar"
|
||||
const val GestureTyping = "gesture_typing"
|
||||
const val Advanced = "advanced"
|
||||
const val Debug = "debug"
|
||||
const val Appearance = "appearance"
|
||||
const val Colors = "colors"
|
||||
const val ColorsNight = "colors_night"
|
||||
const val PersonalDictionary = "personal_dictionary"
|
||||
const val Languages = "languages"
|
||||
val navTarget = MutableStateFlow(Settings)
|
||||
|
||||
private val navScope = CoroutineScope(Dispatchers.Default)
|
||||
fun navigateTo(target: String) {
|
||||
if (navTarget.value == target) {
|
||||
// triggers recompose twice, but that's ok as it's a rare event
|
||||
navTarget.value = Settings
|
||||
navScope.launch { delay(10); navTarget.value = target }
|
||||
} else
|
||||
navTarget.value = target
|
||||
}
|
||||
}
|
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 (later): 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
|
||||
)
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.skydoves.colorpicker.compose.AlphaSlider
|
||||
import com.github.skydoves.colorpicker.compose.BrightnessSlider
|
||||
import com.github.skydoves.colorpicker.compose.ColorEnvelope
|
||||
import com.github.skydoves.colorpicker.compose.HsvColorPicker
|
||||
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
|
||||
|
||||
// todo:
|
||||
// setting from text doesn't work
|
||||
// weird effect on start, did this start with the top row showing colors?
|
||||
// text field doesn't look nice
|
||||
// for initial color picks performance is not good
|
||||
@Composable
|
||||
fun ColorPickerDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
initialColor: Int,
|
||||
title: String,
|
||||
onConfirmed: (Int) -> Unit,
|
||||
) {
|
||||
val controller = rememberColorPickerController()
|
||||
val barHeight = 35.dp
|
||||
var value by remember { mutableStateOf(TextFieldValue(initialColor.toString(16))) }
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = { onConfirmed(controller.selectedColor.value.toArgb()) },
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
Column {
|
||||
Row {
|
||||
Surface(
|
||||
color = Color(initialColor),
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
.padding(start = 10.dp)
|
||||
.height(barHeight))
|
||||
{ }
|
||||
Surface(
|
||||
color = controller.selectedColor.value,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(end = 10.dp)
|
||||
.height(barHeight))
|
||||
{ }
|
||||
}
|
||||
HsvColorPicker(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.6f)
|
||||
.padding(10.dp),
|
||||
controller = controller,
|
||||
onColorChanged = { colorEnvelope: ColorEnvelope ->
|
||||
value = TextFieldValue(colorEnvelope.hexCode)
|
||||
},
|
||||
initialColor = Color(initialColor)
|
||||
)
|
||||
AlphaSlider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
.height(barHeight),
|
||||
controller = controller,
|
||||
)
|
||||
BrightnessSlider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
.height(barHeight),
|
||||
controller = controller,
|
||||
)
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = {
|
||||
val androidColor = kotlin.runCatching { android.graphics.Color.parseColor("#$it") }.getOrNull()
|
||||
if (androidColor != null)
|
||||
controller.selectByColor(Color(androidColor), true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
ColorPickerDialog({}, android.graphics.Color.MAGENTA, "color name", {})
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import helium314.keyboard.latin.R
|
||||
|
||||
// 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),
|
||||
neutralButtonText: String? = null,
|
||||
onNeutral: () -> Unit = { },
|
||||
) {
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = onConfirmed,
|
||||
confirmButtonText = confirmButtonText,
|
||||
cancelButtonText = cancelButtonText,
|
||||
neutralButtonText = neutralButtonText,
|
||||
onNeutral = onNeutral,
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewConfirmDialog() {
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { },
|
||||
onConfirmed = {},
|
||||
neutralButtonText = "hi",
|
||||
confirmButtonText = "I don't care",
|
||||
text = { Text(stringResource(R.string.disable_personalized_dicts_message)) }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import android.graphics.drawable.VectorDrawable
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
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.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.util.TypedValueCompat
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.customIconNames
|
||||
import helium314.keyboard.latin.utils.getStringResourceOrName
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.screens.GetIcon
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Composable
|
||||
fun CustomizeIconsDialog(
|
||||
prefKey: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
val ctx = LocalContext.current
|
||||
var iconsAndNames by remember { mutableStateOf(
|
||||
KeyboardIconsSet.getAllIcons(ctx).keys.map { iconName ->
|
||||
val name = iconName.getStringResourceOrName("", ctx)
|
||||
if (name == iconName) iconName to iconName.getStringResourceOrName("label_", ctx)
|
||||
else iconName to name
|
||||
}.sortedBy { it.second }
|
||||
) }
|
||||
fun reloadItem(iconName: String) {
|
||||
iconsAndNames = iconsAndNames.map { item ->
|
||||
if (item.first == iconName) {
|
||||
item.first to if (item.second.endsWith(" ")) item.second.trimEnd() else item.second + " "
|
||||
}
|
||||
else item
|
||||
}
|
||||
}
|
||||
var showIconDialog: Pair<String, String>? by rememberSaveable { mutableStateOf(null) }
|
||||
var showDeletePrefConfirmDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val prefs = ctx.prefs()
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = { },
|
||||
confirmButtonText = null,
|
||||
cancelButtonText = stringResource(R.string.dialog_close),
|
||||
neutralButtonText = if (prefs.contains(prefKey)) stringResource(R.string.button_default) else null,
|
||||
onNeutral = { showDeletePrefConfirmDialog = true },
|
||||
title = { Text(stringResource(R.string.customize_icons)) },
|
||||
text = {
|
||||
LazyColumn(state = state) {
|
||||
items(iconsAndNames, key = { it.second }) { (iconName, displayName) ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { showIconDialog = iconName to displayName }
|
||||
) {
|
||||
KeyboardIconsSet.instance.GetIcon(iconName)
|
||||
Text(displayName, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
if (showIconDialog != null) {
|
||||
val iconName = showIconDialog!!.first
|
||||
val allIcons = KeyboardIconsSet.getAllIcons(ctx)
|
||||
val iconsForName = allIcons[iconName].orEmpty()
|
||||
val iconsSet = mutableSetOf<Int>()
|
||||
iconsSet.addAll(iconsForName)
|
||||
KeyboardIconsSet.getAllIcons(ctx).forEach { iconsSet.addAll(it.value) }
|
||||
val icons = iconsSet.toList()
|
||||
val initialIcon = KeyboardIconsSet.instance.iconIds[iconName]
|
||||
var selectedIcon by rememberSaveable { mutableStateOf(initialIcon) }
|
||||
|
||||
val gridState = rememberLazyGridState()
|
||||
LaunchedEffect(initialIcon) {
|
||||
val index = icons.indexOf(initialIcon)
|
||||
if (index != -1) gridState.animateScrollToItem(index, -state.layoutInfo.viewportSize.height / 3)
|
||||
}
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = { showIconDialog = null },
|
||||
onConfirmed = {
|
||||
runCatching {
|
||||
val newIcons = customIconNames(prefs).toMutableMap()
|
||||
newIcons[iconName] = selectedIcon?.let { ctx.resources.getResourceEntryName(it) } ?: return@runCatching
|
||||
prefs.edit().putString(prefKey, Json.encodeToString(newIcons)).apply()
|
||||
KeyboardIconsSet.instance.loadIcons(ctx)
|
||||
}
|
||||
reloadItem(iconName)
|
||||
},
|
||||
neutralButtonText = if (customIconNames(prefs).contains(iconName)) stringResource(R.string.button_default) else null,
|
||||
onNeutral = {
|
||||
runCatching {
|
||||
val icons2 = customIconNames(prefs).toMutableMap()
|
||||
icons2.remove(iconName)
|
||||
if (icons2.isEmpty()) prefs.edit().remove(prefKey).apply()
|
||||
else prefs.edit().putString(prefKey, Json.encodeToString(icons2)).apply()
|
||||
KeyboardIconsSet.instance.loadIcons(ctx)
|
||||
}
|
||||
reloadItem(iconName)
|
||||
},
|
||||
title = { Text(showIconDialog!!.second) },
|
||||
text = {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 64.dp),
|
||||
state = gridState
|
||||
) {
|
||||
items(icons, key = { it }) { resId ->
|
||||
val drawable = ContextCompat.getDrawable(ctx, resId)?.mutate() ?: return@items
|
||||
val color = if (resId == selectedIcon) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides color
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(40.dp).clickable { selectedIcon = resId },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (drawable is VectorDrawable)
|
||||
Icon(painterResource(resId), null, Modifier.fillMaxSize(0.8f))
|
||||
else {
|
||||
val px = TypedValueCompat.dpToPx(40f, ctx.resources.displayMetrics).toInt()
|
||||
Icon(drawable.toBitmap(px, px).asImageBitmap(), null, Modifier.fillMaxSize(0.8f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showDeletePrefConfirmDialog) {
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDeletePrefConfirmDialog = false },
|
||||
onConfirmed = {
|
||||
showDeletePrefConfirmDialog = false
|
||||
onDismissRequest()
|
||||
prefs.edit().remove(prefKey).apply()
|
||||
KeyboardIconsSet.instance.loadIcons(ctx)
|
||||
},
|
||||
text = { Text(stringResource(R.string.customize_icons_reset_message)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
KeyboardIconsSet.instance.loadIcons(LocalContext.current)
|
||||
CustomizeIconsDialog(
|
||||
prefKey = "",
|
||||
onDismissRequest = { },
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun InfoDialog(
|
||||
message: String,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
text = { Text(message) },
|
||||
onConfirmed = { },
|
||||
confirmButtonText = null
|
||||
)
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.checkLayout
|
||||
import helium314.keyboard.latin.utils.getCustomLayoutFile
|
||||
import helium314.keyboard.latin.utils.getLayoutDisplayName
|
||||
import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LayoutEditDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
layoutName: String,
|
||||
startContent: String? = null,
|
||||
displayName: String? = null
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
val file = getCustomLayoutFile(layoutName, ctx)
|
||||
val scope = rememberCoroutineScope()
|
||||
var job: Job? = null
|
||||
var showDeleteConfirmation by rememberSaveable { mutableStateOf(false) }
|
||||
TextInputDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = {
|
||||
file.parentFile?.mkdir()
|
||||
file.writeText(it)
|
||||
onCustomLayoutFileListChanged()
|
||||
keyboardNeedsReload = true
|
||||
},
|
||||
confirmButtonText = stringResource(R.string.save),
|
||||
neutralButtonText = if (displayName != null && file.exists()) stringResource(R.string.delete) else null,
|
||||
onNeutral = {
|
||||
if (!file.exists()) return@TextInputDialog
|
||||
file.delete()
|
||||
onCustomLayoutFileListChanged()
|
||||
keyboardNeedsReload = true
|
||||
},
|
||||
initialText = startContent ?: file.readText(),
|
||||
singleLine = false,
|
||||
title = { Text(displayName ?: getLayoutDisplayName(layoutName)) },
|
||||
checkTextValid = {
|
||||
val valid = checkLayout(it, ctx)
|
||||
job?.cancel()
|
||||
if (!valid) {
|
||||
job = scope.launch {
|
||||
delay(3000)
|
||||
val message = Log.getLog(10)
|
||||
.lastOrNull { it.tag == "CustomLayoutUtils" }?.message
|
||||
?.split("\n")?.take(2)?.joinToString("\n")
|
||||
Toast.makeText(ctx, ctx.getString(R.string.layout_error, message), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
valid
|
||||
},
|
||||
modifier = Modifier.imePadding(),
|
||||
// decorFitsSystemWindows = false is necessary so the dialog is not covered by keyboard
|
||||
// but this also stops the background from being darkened... great idea to combine both
|
||||
properties = DialogProperties(decorFitsSystemWindows = false)
|
||||
)
|
||||
if (showDeleteConfirmation)
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDeleteConfirmation = false },
|
||||
onConfirmed = {
|
||||
onDismissRequest()
|
||||
file.delete()
|
||||
onCustomLayoutFileListChanged()
|
||||
keyboardNeedsReload = true
|
||||
},
|
||||
text = { Text(stringResource(R.string.delete_layout, displayName ?: "")) },
|
||||
confirmButtonText = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// taken from StreetComplete
|
||||
@Composable
|
||||
fun <T: Any> 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() },
|
||||
confirmImmediately: Boolean = true,
|
||||
showRadioButtons: Boolean = true,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = { selected?.let { onItemSelected(it) } },
|
||||
confirmButtonText = if (confirmImmediately) null else stringResource(android.R.string.ok),
|
||||
checkOk = { selected != null },
|
||||
modifier = modifier,
|
||||
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 {
|
||||
if (confirmImmediately) {
|
||||
onDismissRequest()
|
||||
onItemSelected(item)
|
||||
}
|
||||
selected = item
|
||||
}
|
||||
.padding(horizontal = 24.dp)
|
||||
.heightIn(min = 40.dp)
|
||||
) {
|
||||
Text(
|
||||
text = getItemName(item),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (showRadioButtons)
|
||||
RadioButton(
|
||||
selected = selected == item,
|
||||
onClick = {
|
||||
if (confirmImmediately) {
|
||||
onDismissRequest()
|
||||
onItemSelected(item)
|
||||
}
|
||||
selected = item
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@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" },
|
||||
)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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 sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@Composable
|
||||
fun <T: Any> ReorderDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirmed: (List<T>) -> Unit,
|
||||
items: List<T>,
|
||||
getKey: (T) -> Any, // actually it's not "Any", but "anything that can be stored in a bundle"
|
||||
displayItem: @Composable (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
onNeutral: () -> Unit = { },
|
||||
neutralButtonText: String? = null,
|
||||
) {
|
||||
var reorderableItems by remember(items) { mutableStateOf(items) }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val dragDropState = rememberReorderableLazyListState(listState) { from, to ->
|
||||
reorderableItems = reorderableItems.toMutableList().apply {
|
||||
add(to.index, removeAt(from.index))
|
||||
}
|
||||
}
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = { onConfirmed(reorderableItems) },
|
||||
onNeutral = onNeutral,
|
||||
neutralButtonText = neutralButtonText,
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
text = {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(reorderableItems, key = getKey) { item ->
|
||||
ReorderableItem(
|
||||
state = dragDropState,
|
||||
key = getKey(item)
|
||||
) { dragging ->
|
||||
val elevation by animateDpAsState(if (dragging) 4.dp else 0.dp)
|
||||
Surface(shadowElevation = elevation) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.longPressDraggableHandle()
|
||||
.heightIn(min = 36.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_drag_indicator),
|
||||
"Reorder",
|
||||
Modifier.padding(end = 6.dp),
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
displayItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
ReorderDialog(
|
||||
onConfirmed = {},
|
||||
onDismissRequest = {},
|
||||
items = listOf(1, 2, 3),
|
||||
displayItem = { Text(it.toString(), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) },
|
||||
getKey = { it.toString() }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import helium314.keyboard.latin.R
|
||||
|
||||
@Composable
|
||||
fun SliderDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDone: (Float) -> Unit,
|
||||
initialValue: Float,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
modifier: Modifier = Modifier,
|
||||
showDefault: Boolean = false,
|
||||
onDefault: () -> Unit = { },
|
||||
onValueChanged: (Float) -> Unit = { },
|
||||
title: (@Composable () -> Unit)? = null,
|
||||
intermediateSteps: Int? = null,
|
||||
positionString: (@Composable (Float) -> String) = { it.toString() },
|
||||
) {
|
||||
var sliderPosition by remember { mutableFloatStateOf(initialValue) }
|
||||
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
neutralButtonText = if (showDefault) stringResource(R.string.button_default) else null,
|
||||
onNeutral = onDefault,
|
||||
onConfirmed = { onDone(sliderPosition) },
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
text = {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyLarge
|
||||
) {
|
||||
Column {
|
||||
if (intermediateSteps == null)
|
||||
Slider(
|
||||
value = sliderPosition,
|
||||
onValueChange = { sliderPosition = it },
|
||||
onValueChangeFinished = { onValueChanged(sliderPosition) },
|
||||
valueRange = range,
|
||||
)
|
||||
else
|
||||
Slider(
|
||||
value = sliderPosition,
|
||||
onValueChange = { sliderPosition = it },
|
||||
onValueChangeFinished = { onValueChanged(sliderPosition) },
|
||||
valueRange = range,
|
||||
steps = intermediateSteps
|
||||
)
|
||||
Text(positionString(sliderPosition))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewSliderDialog() {
|
||||
SliderDialog(
|
||||
onDismissRequest = { },
|
||||
onDone = { },
|
||||
initialValue = 100f,
|
||||
range = 0f..500f,
|
||||
title = { Text("move it") },
|
||||
showDefault = true
|
||||
)
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
// mostly taken from StreetComplete / SCEE
|
||||
/** Dialog with which to input text. OK button is only clickable if [checkTextValid] returns true. */
|
||||
@Composable
|
||||
fun TextInputDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirmed: (text: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
onNeutral: () -> Unit = { },
|
||||
neutralButtonText: String? = null,
|
||||
confirmButtonText: String = stringResource(android.R.string.ok),
|
||||
initialText: String = "",
|
||||
textInputLabel: @Composable (() -> Unit)? = null,
|
||||
singleLine: Boolean = true,
|
||||
keyboardType: KeyboardType = KeyboardType.Unspecified,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
checkTextValid: (text: String) -> Boolean = { it.isNotBlank() }
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
// crappy workaround because otherwise we get a disappearing dialog and a crash
|
||||
// but doesn't work perfectly, dialog doesn't nicely show up again...
|
||||
// todo: understand why it works in ExpandableSearchField, but not here
|
||||
var done by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(initialText) {
|
||||
if (done) return@LaunchedEffect
|
||||
focusRequester.requestFocus()
|
||||
done = true
|
||||
}
|
||||
|
||||
var value by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(initialText, selection = TextRange(if (singleLine) initialText.length else 0)))
|
||||
}
|
||||
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = { onConfirmed(value.text) },
|
||||
confirmButtonText = confirmButtonText,
|
||||
checkOk = { checkTextValid(value.text) },
|
||||
neutralButtonText = neutralButtonText,
|
||||
onNeutral = onNeutral,
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
label = textInputLabel,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
|
||||
singleLine = singleLine
|
||||
)
|
||||
},
|
||||
properties = properties
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TextInputDialog(
|
||||
onDismissRequest = {},
|
||||
onConfirmed = {},
|
||||
title = { Text("Title") },
|
||||
initialText = "some text\nand another line",
|
||||
singleLine = false,
|
||||
textInputLabel = { Text("fill it") }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
* parts taken from Material3 AlertDialog.kt
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import helium314.keyboard.settings.Theme
|
||||
|
||||
// text should be smaller, and background should be darkened
|
||||
@Composable
|
||||
fun ThreeButtonAlertDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirmed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
text: @Composable (() -> Unit)? = null,
|
||||
onNeutral: () -> Unit = { },
|
||||
checkOk: () -> Boolean = { true },
|
||||
confirmButtonText: String? = stringResource(android.R.string.ok),
|
||||
cancelButtonText: String = stringResource(android.R.string.cancel),
|
||||
neutralButtonText: String? = null,
|
||||
properties: DialogProperties = DialogProperties()
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = properties
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.sizeIn(minWidth = 280.dp, maxWidth = 560.dp),
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
contentColor = contentColorFor(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 6.dp)) {
|
||||
title?.let {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleLarge) {
|
||||
Box(Modifier.padding(PaddingValues(bottom = 16.dp))) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {
|
||||
Box(Modifier.weight(weight = 1f, fill = false).padding(bottom = 8.dp)) {
|
||||
text()
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
if (neutralButtonText != null)
|
||||
TextButton(
|
||||
onClick = { onDismissRequest(); onNeutral() }
|
||||
) { Text(neutralButtonText) }
|
||||
Spacer(modifier.weight(1f))
|
||||
TextButton(onClick = onDismissRequest) { Text(cancelButtonText) }
|
||||
if (confirmButtonText != null)
|
||||
TextButton(
|
||||
enabled = checkOk(),
|
||||
onClick = { onDismissRequest(); onConfirmed() },
|
||||
) { Text(confirmButtonText) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
Theme {
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = {},
|
||||
onConfirmed = { },
|
||||
text = { Text("hello") },
|
||||
title = { Text("title") },
|
||||
neutralButtonText = "Default"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.dialogs
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.ToolbarKey
|
||||
import helium314.keyboard.latin.utils.getCodeForToolbarKey
|
||||
import helium314.keyboard.latin.utils.getCodeForToolbarKeyLongClick
|
||||
import helium314.keyboard.latin.utils.getStringResourceOrName
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.latin.utils.readCustomKeyCodes
|
||||
import helium314.keyboard.latin.utils.readCustomLongpressCodes
|
||||
import helium314.keyboard.latin.utils.writeCustomKeyCodes
|
||||
import helium314.keyboard.latin.utils.writeCustomLongpressCodes
|
||||
import helium314.keyboard.settings.screens.GetIcon
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
// todo (later): reading and writing prefs should be done in the preference, or at least with the provided (single!) pref key
|
||||
@Composable
|
||||
fun ToolbarKeysCustomizer(
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
var showKeyCustomizer: ToolbarKey? by rememberSaveable { mutableStateOf(null) }
|
||||
var showDeletePrefConfirmDialog by rememberSaveable { mutableStateOf(false) }
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
cancelButtonText = stringResource(R.string.dialog_close),
|
||||
confirmButtonText = null,
|
||||
onConfirmed = { },
|
||||
neutralButtonText = if (readCustomKeyCodes(prefs).isNotEmpty() || readCustomLongpressCodes(prefs).isNotEmpty()) stringResource(R.string.button_default) else null,
|
||||
onNeutral = { showDeletePrefConfirmDialog = true },
|
||||
title = { Text(stringResource(R.string.customize_toolbar_key_codes)) },
|
||||
text = {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(ToolbarKey.entries) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { showKeyCustomizer = it }.fillParentMaxWidth()
|
||||
) {
|
||||
KeyboardIconsSet.instance.GetIcon(it.name)
|
||||
Text(it.name.lowercase().getStringResourceOrName("", ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
if (showKeyCustomizer != null) {
|
||||
val shownKey = showKeyCustomizer
|
||||
if (shownKey != null)
|
||||
ToolbarKeyCustomizer(shownKey) { showKeyCustomizer = null }
|
||||
}
|
||||
if (showDeletePrefConfirmDialog)
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDeletePrefConfirmDialog = false },
|
||||
onConfirmed = {
|
||||
showDeletePrefConfirmDialog = false
|
||||
onDismissRequest()
|
||||
prefs.edit {
|
||||
remove(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES)
|
||||
remove(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES)
|
||||
}
|
||||
},
|
||||
text = { Text(stringResource(R.string.customize_toolbar_key_code_reset_message)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToolbarKeyCustomizer(
|
||||
key: ToolbarKey,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
var code by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(getCodeForToolbarKey(key).toString())) }
|
||||
var longPressCode by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(getCodeForToolbarKeyLongClick(key).toString())) }
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirmed = {
|
||||
writeCustomKeyCodes(prefs, readCustomKeyCodes(prefs) + (key.name to checkCode(code)))
|
||||
writeCustomLongpressCodes(prefs, readCustomLongpressCodes(prefs) + (key.name to checkCode(longPressCode)))
|
||||
},
|
||||
checkOk = { checkCode(code) != null && checkCode(longPressCode) != null },
|
||||
neutralButtonText = if (readCustomKeyCodes(prefs).containsKey(key.name) || readCustomLongpressCodes(prefs).containsKey(key.name))
|
||||
stringResource(R.string.button_default)
|
||||
else null,
|
||||
onNeutral = {
|
||||
val keys = readCustomKeyCodes(prefs).toMutableMap()
|
||||
keys.remove(key.name)
|
||||
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, Json.encodeToString(keys)).apply()
|
||||
val longpressKeys = readCustomLongpressCodes(prefs).toMutableMap()
|
||||
longpressKeys.remove(key.name)
|
||||
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES, Json.encodeToString(longpressKeys)).apply()
|
||||
},
|
||||
title = { Text(key.name.lowercase().getStringResourceOrName("", ctx)) },
|
||||
text = {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.key_code), Modifier.weight(0.5f))
|
||||
TextField(
|
||||
value = code,
|
||||
onValueChange = { code = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.long_press_code), Modifier.weight(0.5f))
|
||||
TextField(
|
||||
value = longPressCode,
|
||||
onValueChange = { longPressCode = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewToolbarKeyCustomizer() {
|
||||
Settings.init(LocalContext.current)
|
||||
ToolbarKeyCustomizer(ToolbarKey.CUT) { }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewToolbarKeysCustomizer() {
|
||||
Settings.init(LocalContext.current)
|
||||
KeyboardIconsSet.instance.loadIcons(LocalContext.current)
|
||||
ToolbarKeysCustomizer { }
|
||||
}
|
||||
|
||||
private fun checkCode(code: TextFieldValue) = runCatching {
|
||||
code.text.toIntOrNull()?.takeIf { it.checkAndConvertCode() <= Char.MAX_VALUE.code }
|
||||
}.getOrNull()
|
|
@ -0,0 +1,128 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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 helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.common.FileUtils
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||
import helium314.keyboard.settings.dialogs.InfoDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun BackgroundImagePref(setting: Setting, isLandscape: Boolean) {
|
||||
var showDayNightDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSelectionDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showErrorDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var isNight by rememberSaveable { mutableStateOf(false) }
|
||||
val ctx = LocalContext.current
|
||||
fun getFile() = Settings.getCustomBackgroundFile(ctx, isNight, isLandscape)
|
||||
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0) // necessary to reload dayNightPref
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val dayNightPref = Settings.readDayNightPref(ctx.prefs(), ctx.resources)
|
||||
if (!dayNightPref)
|
||||
isNight = false
|
||||
val scope = rememberCoroutineScope()
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
showSelectionDialog = false
|
||||
showDayNightDialog = false
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!setBackgroundImage(ctx, uri, isNight, isLandscape))
|
||||
showErrorDialog = true
|
||||
}
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("image/*")
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = {
|
||||
if (dayNightPref) {
|
||||
showDayNightDialog = true
|
||||
} else if (!getFile().exists()) {
|
||||
launcher.launch(intent)
|
||||
} else {
|
||||
showSelectionDialog = true
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showDayNightDialog) {
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDayNightDialog = false },
|
||||
onConfirmed = {
|
||||
isNight = false
|
||||
if (getFile().exists())
|
||||
showSelectionDialog = true
|
||||
else launcher.launch(intent)
|
||||
},
|
||||
confirmButtonText = stringResource(R.string.day_or_night_day),
|
||||
cancelButtonText = "",
|
||||
onNeutral = {
|
||||
isNight = true
|
||||
if (getFile().exists())
|
||||
showSelectionDialog = true
|
||||
else launcher.launch(intent)
|
||||
},
|
||||
neutralButtonText = stringResource(R.string.day_or_night_night),
|
||||
title = { Text(stringResource(R.string.day_or_night_image)) },
|
||||
)
|
||||
}
|
||||
if (showSelectionDialog) {
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showSelectionDialog = false },
|
||||
title = { Text(stringResource(R.string.customize_background_image)) },
|
||||
confirmButtonText = stringResource(R.string.button_load_custom),
|
||||
onConfirmed = { launcher.launch(intent) },
|
||||
neutralButtonText = stringResource(R.string.delete),
|
||||
onNeutral = {
|
||||
getFile().delete()
|
||||
Settings.clearCachedBackgroundImages()
|
||||
keyboardNeedsReload = true
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showErrorDialog) {
|
||||
InfoDialog(stringResource(R.string.file_read_error)) { showErrorDialog = false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBackgroundImage(ctx: Context, uri: Uri, isNight: Boolean, isLandscape: Boolean): Boolean {
|
||||
val imageFile = Settings.getCustomBackgroundFile(ctx, isNight, isLandscape)
|
||||
FileUtils.copyContentUriToNewFile(uri, ctx, imageFile)
|
||||
keyboardNeedsReload = true
|
||||
try {
|
||||
BitmapFactory.decodeFile(imageFile.absolutePath)
|
||||
} catch (_: Exception) {
|
||||
imageFile.delete()
|
||||
return false
|
||||
}
|
||||
Settings.clearCachedBackgroundImages()
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.preference.PreferenceManager
|
||||
import helium314.keyboard.dictionarypack.DictionaryPackConstants
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMBER
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMPAD
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_NUMPAD_LANDSCAPE
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_PHONE_SYMBOLS
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_ARABIC
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.LAYOUT_SYMBOLS_SHIFTED
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.checkVersionUpgrade
|
||||
import helium314.keyboard.latin.common.FileUtils
|
||||
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.settings.USER_DICTIONARY_SUFFIX
|
||||
import helium314.keyboard.latin.utils.AdditionalSubtypeUtils
|
||||
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
|
||||
import helium314.keyboard.latin.utils.DeviceProtectedUtils
|
||||
import helium314.keyboard.latin.utils.ExecutorUtils
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.onCustomLayoutFileListChanged
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.latin.utils.reloadEnabledSubtypes
|
||||
import helium314.keyboard.latin.utils.updateAdditionalSubtypes
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||
import helium314.keyboard.settings.dialogs.InfoDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@Composable
|
||||
fun BackupRestorePreference(setting: Setting) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
var error: String? by rememberSaveable { mutableStateOf(null) }
|
||||
val backupFilePatterns by lazy { listOf(
|
||||
"blacklists/.*\\.txt".toRegex(),
|
||||
"layouts/$CUSTOM_LAYOUT_PREFIX+\\..{0,4}".toRegex(), // can't expect a period at the end, as this would break restoring older backups
|
||||
"dicts/.*/.*user\\.dict".toRegex(),
|
||||
"UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(),
|
||||
"custom_background_image.*".toRegex(),
|
||||
"custom_font".toRegex(),
|
||||
) }
|
||||
val backupLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
// zip all files matching the backup patterns
|
||||
// essentially this is the typed words information, and user-added dictionaries
|
||||
val filesDir = ctx.filesDir ?: return@rememberLauncherForActivityResult
|
||||
val filesPath = filesDir.path + File.separator
|
||||
val files = mutableListOf<File>()
|
||||
filesDir.walk().forEach { file ->
|
||||
val path = file.path.replace(filesPath, "")
|
||||
if (backupFilePatterns.any { path.matches(it) })
|
||||
files.add(file)
|
||||
}
|
||||
val protectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx)
|
||||
val protectedFilesPath = protectedFilesDir.path + File.separator
|
||||
val protectedFiles = mutableListOf<File>()
|
||||
protectedFilesDir.walk().forEach { file ->
|
||||
val path = file.path.replace(protectedFilesPath, "")
|
||||
if (backupFilePatterns.any { path.matches(it) })
|
||||
protectedFiles.add(file)
|
||||
}
|
||||
val wait = CountDownLatch(1)
|
||||
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute {
|
||||
try {
|
||||
ctx.getActivity()?.contentResolver?.openOutputStream(uri)?.use { os ->
|
||||
// write files to zip
|
||||
val zipStream = ZipOutputStream(os)
|
||||
files.forEach {
|
||||
val fileStream = FileInputStream(it).buffered()
|
||||
zipStream.putNextEntry(ZipEntry(it.path.replace(filesPath, "")))
|
||||
fileStream.copyTo(zipStream, 1024)
|
||||
fileStream.close()
|
||||
zipStream.closeEntry()
|
||||
}
|
||||
protectedFiles.forEach {
|
||||
val fileStream = FileInputStream(it).buffered()
|
||||
zipStream.putNextEntry(ZipEntry(it.path.replace(protectedFilesDir.path, "unprotected")))
|
||||
fileStream.copyTo(zipStream, 1024)
|
||||
fileStream.close()
|
||||
zipStream.closeEntry()
|
||||
}
|
||||
zipStream.putNextEntry(ZipEntry(PREFS_FILE_NAME))
|
||||
settingsToJsonStream(prefs.all, zipStream)
|
||||
zipStream.closeEntry()
|
||||
zipStream.putNextEntry(ZipEntry(PROTECTED_PREFS_FILE_NAME))
|
||||
settingsToJsonStream(PreferenceManager.getDefaultSharedPreferences(ctx).all, zipStream)
|
||||
zipStream.closeEntry()
|
||||
zipStream.close()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
error = "b" + t.message
|
||||
Log.w("AdvancedScreen", "error during backup", t)
|
||||
} finally {
|
||||
wait.countDown()
|
||||
}
|
||||
}
|
||||
wait.await()
|
||||
}
|
||||
val restoreLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
val wait = CountDownLatch(1)
|
||||
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute {
|
||||
try {
|
||||
ctx.getActivity()?.contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zip ->
|
||||
var entry: ZipEntry? = zip.nextEntry
|
||||
val filesDir = ctx.filesDir?.path ?: return@execute
|
||||
val deviceProtectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx).path
|
||||
Settings.getInstance().stopListener()
|
||||
while (entry != null) {
|
||||
if (entry.name.startsWith("unprotected${File.separator}")) {
|
||||
val adjustedName = entry.name.substringAfter("unprotected${File.separator}")
|
||||
if (backupFilePatterns.any { adjustedName.matches(it) }) {
|
||||
val targetFileName = upgradeFileNames(adjustedName)
|
||||
val file = File(deviceProtectedFilesDir, targetFileName)
|
||||
FileUtils.copyStreamToNewFile(zip, file)
|
||||
}
|
||||
} else if (backupFilePatterns.any { entry!!.name.matches(it) }) {
|
||||
val targetFileName = upgradeFileNames(entry.name)
|
||||
val file = File(filesDir, targetFileName)
|
||||
FileUtils.copyStreamToNewFile(zip, file)
|
||||
} else if (entry.name == PREFS_FILE_NAME) {
|
||||
val prefLines = String(zip.readBytes()).split("\n")
|
||||
prefs.edit().clear().apply()
|
||||
readJsonLinesToSettings(prefLines, prefs)
|
||||
} else if (entry.name == PROTECTED_PREFS_FILE_NAME) {
|
||||
val prefLines = String(zip.readBytes()).split("\n")
|
||||
val protectedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
protectedPrefs.edit().clear().apply()
|
||||
readJsonLinesToSettings(prefLines, protectedPrefs)
|
||||
}
|
||||
zip.closeEntry()
|
||||
entry = zip.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
error = "r" + t.message
|
||||
Log.w("AdvancedScreen", "error during restore", t)
|
||||
} finally {
|
||||
wait.countDown()
|
||||
}
|
||||
}
|
||||
wait.await()
|
||||
checkVersionUpgrade(ctx)
|
||||
Settings.getInstance().startListener()
|
||||
val additionalSubtypes = Settings.readPrefAdditionalSubtypes(prefs, ctx.resources)
|
||||
updateAdditionalSubtypes(AdditionalSubtypeUtils.createAdditionalSubtypesArray(additionalSubtypes))
|
||||
reloadEnabledSubtypes(ctx)
|
||||
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
|
||||
ctx.getActivity()?.sendBroadcast(newDictBroadcast)
|
||||
onCustomLayoutFileListChanged()
|
||||
(ctx.getActivity() as? SettingsActivity)?.prefChanged?.value = 210 // for settings reload
|
||||
keyboardNeedsReload = true
|
||||
}
|
||||
Preference(name = setting.title, onClick = { showDialog = true })
|
||||
if (showDialog) {
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
title = { Text(stringResource(R.string.backup_restore_title)) },
|
||||
text = { Text(stringResource(R.string.backup_restore_message)) },
|
||||
confirmButtonText = stringResource(R.string.button_backup),
|
||||
neutralButtonText = stringResource(R.string.button_restore),
|
||||
onNeutral = {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("application/zip")
|
||||
restoreLauncher.launch(intent)
|
||||
},
|
||||
onConfirmed = {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
ctx.getString(R.string.english_ime_name)
|
||||
.replace(" ", "_") + "_backup.zip"
|
||||
)
|
||||
.setType("application/zip")
|
||||
backupLauncher.launch(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (error != null) {
|
||||
InfoDialog(
|
||||
if (error!!.startsWith("b"))
|
||||
stringResource(R.string.backup_error, error!!.drop(1))
|
||||
else stringResource(R.string.restore_error, error!!.drop(1))
|
||||
) { error = null }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // it is checked... but whatever (except string set, because can't check for that))
|
||||
private fun settingsToJsonStream(settings: Map<String?, Any?>, out: OutputStream) {
|
||||
val booleans = settings.filter { it.key is String && it.value is Boolean } as Map<String, Boolean>
|
||||
val ints = settings.filter { it.key is String && it.value is Int } as Map<String, Int>
|
||||
val longs = settings.filter { it.key is String && it.value is Long } as Map<String, Long>
|
||||
val floats = settings.filter { it.key is String && it.value is Float } as Map<String, Float>
|
||||
val strings = settings.filter { it.key is String && it.value is String } as Map<String, String>
|
||||
val stringSets = settings.filter { it.key is String && it.value is Set<*> } as Map<String, Set<String>>
|
||||
// now write
|
||||
out.write("boolean settings\n".toByteArray())
|
||||
out.write(Json.encodeToString(booleans).toByteArray())
|
||||
out.write("\nint settings\n".toByteArray())
|
||||
out.write(Json.encodeToString(ints).toByteArray())
|
||||
out.write("\nlong settings\n".toByteArray())
|
||||
out.write(Json.encodeToString(longs).toByteArray())
|
||||
out.write("\nfloat settings\n".toByteArray())
|
||||
out.write(Json.encodeToString(floats).toByteArray())
|
||||
out.write("\nstring settings\n".toByteArray())
|
||||
out.write(Json.encodeToString(strings).toByteArray())
|
||||
out.write("\nstring set settings\n".toByteArray())
|
||||
out.write(Json.encodeToString(stringSets).toByteArray())
|
||||
}
|
||||
|
||||
private fun readJsonLinesToSettings(list: List<String>, prefs: SharedPreferences): Boolean {
|
||||
val i = list.iterator()
|
||||
val e = prefs.edit()
|
||||
try {
|
||||
while (i.hasNext()) {
|
||||
when (i.next()) {
|
||||
"boolean settings" -> Json.decodeFromString<Map<String, Boolean>>(i.next()).forEach { e.putBoolean(it.key, it.value) }
|
||||
"int settings" -> Json.decodeFromString<Map<String, Int>>(i.next()).forEach { e.putInt(it.key, it.value) }
|
||||
"long settings" -> Json.decodeFromString<Map<String, Long>>(i.next()).forEach { e.putLong(it.key, it.value) }
|
||||
"float settings" -> Json.decodeFromString<Map<String, Float>>(i.next()).forEach { e.putFloat(it.key, it.value) }
|
||||
"string settings" -> Json.decodeFromString<Map<String, String>>(i.next()).forEach { e.putString(it.key, it.value) }
|
||||
"string set settings" -> Json.decodeFromString<Map<String, Set<String>>>(i.next()).forEach { e.putStringSet(it.key, it.value) }
|
||||
}
|
||||
}
|
||||
e.apply()
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// todo (later): remove this when new package name has been in use for long enough, this is only for migrating from old openboard name
|
||||
private fun upgradeFileNames(originalName: String): String {
|
||||
return when {
|
||||
originalName.endsWith(USER_DICTIONARY_SUFFIX) -> {
|
||||
// replace directory after switch to language tag
|
||||
val dirName = originalName.substringAfter(File.separator).substringBefore(File.separator)
|
||||
originalName.replace(dirName, dirName.constructLocale().toLanguageTag())
|
||||
}
|
||||
originalName.startsWith("blacklists") -> {
|
||||
// replace file name after switch to language tag
|
||||
val fileName = originalName.substringAfter("blacklists${File.separator}").substringBefore(".txt")
|
||||
originalName.replace(fileName, fileName.constructLocale().toLanguageTag())
|
||||
}
|
||||
originalName.startsWith("layouts") -> {
|
||||
// replace file name after switch to language tag, but only if it's not a layout
|
||||
val localeString = originalName.substringAfter(".").substringBefore(".")
|
||||
if (localeString in listOf(LAYOUT_SYMBOLS, LAYOUT_SYMBOLS_SHIFTED, LAYOUT_SYMBOLS_ARABIC, LAYOUT_NUMBER, LAYOUT_NUMPAD, LAYOUT_NUMPAD_LANDSCAPE, LAYOUT_PHONE, LAYOUT_PHONE_SYMBOLS))
|
||||
return originalName // it's a layout!
|
||||
val locale = localeString.constructLocale()
|
||||
if (locale.toLanguageTag() != "und")
|
||||
originalName.replace(localeString, locale.toLanguageTag())
|
||||
else
|
||||
originalName // no valid locale -> must be symbols layout, don't change
|
||||
}
|
||||
originalName.startsWith("UserHistoryDictionary") -> {
|
||||
val localeString = originalName.substringAfter(".").substringBefore(".")
|
||||
val locale = localeString.constructLocale()
|
||||
originalName.replace(localeString, locale.toLanguageTag())
|
||||
}
|
||||
else -> originalName
|
||||
}
|
||||
}
|
||||
|
||||
private const val PREFS_FILE_NAME = "preferences.json"
|
||||
private const val PROTECTED_PREFS_FILE_NAME = "protected_preferences.json"
|
|
@ -0,0 +1,76 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.common.FileUtils
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.DeviceProtectedUtils
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||
import helium314.keyboard.settings.dialogs.InfoDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun CustomFontPreference(setting: Setting) {
|
||||
val ctx = LocalContext.current
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showErrorDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val fontFile = Settings.getCustomFontFile(ctx)
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
val uri = it.data?.data ?: return@rememberLauncherForActivityResult
|
||||
val tempFile = File(DeviceProtectedUtils.getFilesDir(ctx), "temp_file")
|
||||
FileUtils.copyContentUriToNewFile(uri, ctx, tempFile)
|
||||
try {
|
||||
Typeface.createFromFile(tempFile)
|
||||
fontFile.delete()
|
||||
tempFile.renameTo(fontFile)
|
||||
Settings.clearCachedTypeface()
|
||||
keyboardNeedsReload = true
|
||||
} catch (_: Exception) {
|
||||
showErrorDialog = true
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("*/*")
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = {
|
||||
if (fontFile.exists())
|
||||
showDialog = true
|
||||
else launcher.launch(intent)
|
||||
},
|
||||
)
|
||||
if (showDialog)
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
onConfirmed = { launcher.launch(intent) },
|
||||
onNeutral = {
|
||||
fontFile.delete()
|
||||
Settings.clearCachedTypeface()
|
||||
keyboardNeedsReload = true
|
||||
},
|
||||
neutralButtonText = stringResource(R.string.delete),
|
||||
confirmButtonText = stringResource(R.string.load),
|
||||
title = { Text(stringResource(R.string.custom_font)) }
|
||||
)
|
||||
if (showErrorDialog)
|
||||
InfoDialog(stringResource(R.string.file_read_error)) { showErrorDialog = false }
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
|
||||
import helium314.keyboard.latin.utils.getCustomLayoutFiles
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.dialogs.LayoutEditDialog
|
||||
import helium314.keyboard.settings.dialogs.ListPickerDialog
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun LayoutEditPreference(
|
||||
setting: Setting,
|
||||
items: List<String>,
|
||||
getItemName: @Composable (String) -> String,
|
||||
getDefaultLayout: @Composable (String?) -> String?,
|
||||
) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val ctx = LocalContext.current
|
||||
var layout: String? by rememberSaveable { mutableStateOf(null) }
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
if (showDialog) {
|
||||
ListPickerDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
showRadioButtons = false,
|
||||
confirmImmediately = true,
|
||||
items = items,
|
||||
getItemName = getItemName,
|
||||
onItemSelected = { layout = it },
|
||||
title = { Text(setting.title) }
|
||||
)
|
||||
}
|
||||
if (layout != null) {
|
||||
val customLayoutName = getCustomLayoutFiles(ctx).firstOrNull {
|
||||
if (layout!!.startsWith(CUSTOM_LAYOUT_PREFIX))
|
||||
it.name.startsWith("$layout.")
|
||||
else it.name.startsWith("$CUSTOM_LAYOUT_PREFIX$layout.")
|
||||
}?.name
|
||||
val originalLayout = if (customLayoutName != null) null
|
||||
else getDefaultLayout(layout)?.let { ctx.assets.open("layouts" + File.separator + it).reader().readText() }
|
||||
LayoutEditDialog(
|
||||
layoutName = customLayoutName ?: "$CUSTOM_LAYOUT_PREFIX$layout.",
|
||||
startContent = originalLayout,
|
||||
displayName = getItemName(layout!!),
|
||||
onDismissRequest = { layout = null }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.common.FileUtils
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.ChecksumCalculator
|
||||
import helium314.keyboard.latin.utils.JniUtils
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
@Composable
|
||||
fun LoadGestureLibPreference(setting: Setting) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
val abi = Build.SUPPORTED_ABIS[0]
|
||||
val libFile = File(ctx.filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME)
|
||||
fun renameToLibFileAndRestart(file: File, checksum: String) {
|
||||
libFile.delete()
|
||||
// store checksum in default preferences (soo JniUtils)
|
||||
prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit()
|
||||
file.renameTo(libFile)
|
||||
Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded
|
||||
}
|
||||
var tempFilePath: String? by rememberSaveable { mutableStateOf(null) }
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
val tmpfile = File(ctx.filesDir.absolutePath + File.separator + "tmplib")
|
||||
try {
|
||||
val otherTemporaryFile = File(ctx.filesDir.absolutePath + File.separator + "tmpfile")
|
||||
FileUtils.copyContentUriToNewFile(uri, ctx, otherTemporaryFile)
|
||||
val inputStream = FileInputStream(otherTemporaryFile)
|
||||
val outputStream = FileOutputStream(tmpfile)
|
||||
outputStream.use {
|
||||
tmpfile.setReadOnly() // as per recommendations in https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
|
||||
FileUtils.copyStreamToOtherStream(inputStream, it)
|
||||
}
|
||||
otherTemporaryFile.delete()
|
||||
|
||||
val checksum = ChecksumCalculator.checksum(tmpfile.inputStream()) ?: ""
|
||||
if (checksum == JniUtils.expectedDefaultChecksum()) {
|
||||
renameToLibFileAndRestart(tmpfile, checksum)
|
||||
} else {
|
||||
tempFilePath = tmpfile.absolutePath
|
||||
AlertDialog.Builder(ctx)
|
||||
.setMessage(ctx.getString(R.string.checksum_mismatch_message, abi))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> renameToLibFileAndRestart(tmpfile, checksum) }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> tmpfile.delete() }
|
||||
.show()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
tmpfile.delete()
|
||||
// should inform user, but probably the issues will only come when reading the library
|
||||
}
|
||||
}
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
if (showDialog) {
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
onConfirmed = {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("application/octet-stream")
|
||||
launcher.launch(intent)
|
||||
},
|
||||
title = { Text(stringResource(R.string.load_gesture_library)) },
|
||||
text = { Text(stringResource(R.string.load_gesture_library_message, abi)) },
|
||||
neutralButtonText = if (libFile.exists()) stringResource(R.string.load_gesture_library_button_delete) else null,
|
||||
onNeutral = {
|
||||
libFile.delete()
|
||||
prefs.edit().remove(Settings.PREF_LIBRARY_CHECKSUM).commit()
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (tempFilePath != null)
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = {
|
||||
File(tempFilePath!!).delete()
|
||||
tempFilePath = null
|
||||
},
|
||||
text = { Text(stringResource(R.string.checksum_mismatch_message, abi)) },
|
||||
onConfirmed = {
|
||||
val tempFile = File(tempFilePath!!)
|
||||
renameToLibFileAndRestart(tempFile, ChecksumCalculator.checksum(tempFile.inputStream()) ?: "")
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
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.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.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
|
||||
// partially taken from StreetComplete / SCEE
|
||||
|
||||
@Composable
|
||||
fun PreferenceCategory(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column {
|
||||
HorizontalDivider()
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Preference(
|
||||
name: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
description: String? = null,
|
||||
@DrawableRes icon: Int? = null,
|
||||
value: @Composable (RowScope.() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.heightIn(min = 44.dp)
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (icon != null)
|
||||
Icon(painterResource(icon), name, modifier = Modifier.size(36.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = name, style = MaterialTheme.typography.bodyLarge)
|
||||
if (description != null) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyMedium,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant
|
||||
) {
|
||||
Text(
|
||||
text = description,
|
||||
modifier = Modifier.padding(top = 2.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,
|
||||
) { value() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreferencePreview() {
|
||||
Surface {
|
||||
Column {
|
||||
PreferenceCategory("Preference Category")
|
||||
Preference(
|
||||
name = "Preference",
|
||||
onClick = {},
|
||||
)
|
||||
Preference(
|
||||
name = "Preference with icon",
|
||||
onClick = {},
|
||||
icon = R.drawable.ic_settings_about_foreground
|
||||
)
|
||||
SliderPreference(
|
||||
name = "SliderPreference",
|
||||
key = "",
|
||||
default = 1,
|
||||
description = { it.toString() },
|
||||
range = -5f..5f
|
||||
)
|
||||
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",
|
||||
key = "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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.utils.getStringResourceOrName
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.dialogs.ReorderDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import helium314.keyboard.settings.screens.GetIcon
|
||||
|
||||
@Composable
|
||||
fun ReorderSwitchPreference(setting: Setting, default: String) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
Preference(
|
||||
name = setting.title,
|
||||
description = setting.description,
|
||||
onClick = { showDialog = true },
|
||||
)
|
||||
if (showDialog) {
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
val items = prefs.getString(setting.key, default)!!.split(";").mapTo(ArrayList()) {
|
||||
val both = it.split(",")
|
||||
KeyAndState(both.first(), both.last().toBoolean())
|
||||
}
|
||||
ReorderDialog(
|
||||
onConfirmed = { reorderedItems ->
|
||||
val value = reorderedItems.joinToString(";") { it.name + "," + it.state }
|
||||
prefs.edit().putString(setting.key, value).apply()
|
||||
keyboardNeedsReload = true
|
||||
},
|
||||
onDismissRequest = { showDialog = false },
|
||||
onNeutral = { prefs.edit().remove(setting.key).apply() },
|
||||
neutralButtonText = if (prefs.contains(setting.key)) stringResource(R.string.button_default) else null,
|
||||
items = items,
|
||||
title = { Text(setting.title) },
|
||||
displayItem = { item ->
|
||||
var checked by rememberSaveable { mutableStateOf(item.state) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
KeyboardIconsSet.instance.GetIcon(item.name)
|
||||
val text = item.name.lowercase().getStringResourceOrName("", ctx)
|
||||
Text(text, Modifier.weight(1f))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = { item.state = it; checked = it }
|
||||
)
|
||||
}
|
||||
},
|
||||
getKey = { it.name }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyAndState(var name: String, var state: Boolean)
|
|
@ -0,0 +1,129 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.edit
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.dialogs.ListPickerDialog
|
||||
import helium314.keyboard.settings.dialogs.SliderDialog
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
/** Slider preference for Int or Float (weird casting stuff, but should be fine) */
|
||||
fun <T: Number> SliderPreference(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
key: String,
|
||||
description: @Composable (T) -> String,
|
||||
default: T,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
stepSize: Int? = null,
|
||||
onValueChanged: (Float) -> Unit = { },
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val initialValue = if (default is Int || default is Float)
|
||||
getPrefOfType(prefs, key, default)
|
||||
else throw IllegalArgumentException("only float and int are supported")
|
||||
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
Preference(
|
||||
name = name,
|
||||
onClick = { showDialog = true },
|
||||
modifier = modifier,
|
||||
description = description(initialValue)
|
||||
)
|
||||
if (showDialog)
|
||||
SliderDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
onDone = {
|
||||
if (default is Int) prefs.edit().putInt(key, it.toInt()).apply()
|
||||
else prefs.edit().putFloat(key, it).apply()
|
||||
},
|
||||
initialValue = initialValue.toFloat(),
|
||||
range = range,
|
||||
positionString = {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
description((if (default is Int) it.roundToInt() else it) as T)
|
||||
},
|
||||
onValueChanged = onValueChanged,
|
||||
showDefault = true,
|
||||
onDefault = { prefs.edit().remove(key).apply() },
|
||||
intermediateSteps = stepSize?.let {
|
||||
// this is not nice, but slider wants it like this...
|
||||
((range.endInclusive - range.start) / it - 1).toInt()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
// just in here so we can keep getPrefOfType private... rename file?
|
||||
fun <T: Any> ListPreference(
|
||||
setting: Setting,
|
||||
items: List<Pair<String, T>>,
|
||||
default: T,
|
||||
onChanged: (T) -> Unit = { }
|
||||
) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val prefs = LocalContext.current.prefs()
|
||||
val selected = items.firstOrNull { it.second == getPrefOfType(prefs, setting.key, default) }
|
||||
Preference(
|
||||
name = setting.title,
|
||||
description = selected?.first,
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
if (showDialog) {
|
||||
ListPickerDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
items = items,
|
||||
onItemSelected = {
|
||||
if (it == selected) return@ListPickerDialog
|
||||
putPrefOfType(prefs, setting.key, it.second)
|
||||
onChanged(it.second)
|
||||
},
|
||||
selectedItem = selected,
|
||||
title = { Text(setting.title) },
|
||||
getItemName = { it.first }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T: Any> getPrefOfType(prefs: SharedPreferences, key: String, default: T): T =
|
||||
when (default) {
|
||||
is String -> prefs.getString(key, default)
|
||||
is Int -> prefs.getInt(key, default)
|
||||
is Long -> prefs.getLong(key, default)
|
||||
is Float -> prefs.getFloat(key, default)
|
||||
is Boolean -> prefs.getBoolean(key, default)
|
||||
else -> throw IllegalArgumentException("unknown type ${default.javaClass}")
|
||||
} as T
|
||||
|
||||
private fun <T: Any> putPrefOfType(prefs: SharedPreferences, key: String, value: T) =
|
||||
prefs.edit {
|
||||
when (value) {
|
||||
is String -> putString(key, value)
|
||||
is Int -> putInt(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
is Boolean -> putBoolean(key, value)
|
||||
else -> throw IllegalArgumentException("unknown type ${value.javaClass}")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.preferences
|
||||
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
setting: Setting,
|
||||
default: Boolean,
|
||||
allowCheckedChange: (Boolean) -> Boolean = { true },
|
||||
onCheckedChange: (Boolean) -> Unit = { }
|
||||
) {
|
||||
SwitchPreference(
|
||||
name = setting.title,
|
||||
description = setting.description,
|
||||
key = setting.key,
|
||||
default = default,
|
||||
allowCheckedChange = allowCheckedChange,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
key: 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 = ctx.prefs()
|
||||
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
var value = prefs.getBoolean(key, default)
|
||||
fun switched(newValue: Boolean) {
|
||||
if (!allowCheckedChange(newValue)) {
|
||||
value = !newValue
|
||||
return
|
||||
}
|
||||
value = newValue
|
||||
prefs.edit().putBoolean(key, newValue).apply()
|
||||
onCheckedChange(newValue)
|
||||
}
|
||||
Preference(
|
||||
name = name,
|
||||
onClick = { switched(!value) },
|
||||
modifier = modifier,
|
||||
description = description
|
||||
) {
|
||||
Switch(
|
||||
checked = value,
|
||||
onCheckedChange = { switched(it) },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.net.toUri
|
||||
import helium314.keyboard.latin.BuildConfig
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.DebugSettings
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.SpannableStringUtils
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.SettingsWithoutKey
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.Theme
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AboutScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val items = listOf(
|
||||
SettingsWithoutKey.APP,
|
||||
SettingsWithoutKey.VERSION,
|
||||
SettingsWithoutKey.LICENSE,
|
||||
SettingsWithoutKey.HIDDEN_FEATURES,
|
||||
SettingsWithoutKey.GITHUB,
|
||||
SettingsWithoutKey.SAVE_LOG
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_about),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
fun createAboutSettings(context: Context) = listOf(
|
||||
Setting(context, SettingsWithoutKey.APP, R.string.english_ime_name, R.string.app_slogan) {
|
||||
Preference(
|
||||
name = it.title,
|
||||
description = it.description,
|
||||
onClick = { },
|
||||
icon = R.drawable.ic_launcher_foreground // use the bitmap trick here if we really want the colored icon
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.VERSION, R.string.version) {
|
||||
var count by rememberSaveable { mutableIntStateOf(0) }
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
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
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.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
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.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 = 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
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.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
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.SAVE_LOG, R.string.save_log) { setting ->
|
||||
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 = setting.title,
|
||||
description = setting.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
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
AboutScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.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 helium314.keyboard.keyboard.KeyboardActionListener
|
||||
import helium314.keyboard.keyboard.KeyboardLayoutSet
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.RawKeyboardParser
|
||||
import helium314.keyboard.latin.BuildConfig
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.SystemBroadcastReceiver
|
||||
import helium314.keyboard.latin.common.splitOnWhitespace
|
||||
import helium314.keyboard.latin.settings.DebugSettings
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_NORMAL
|
||||
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS
|
||||
import helium314.keyboard.latin.utils.CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED
|
||||
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
|
||||
import helium314.keyboard.latin.utils.getStringResourceOrName
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.preferences.ListPreference
|
||||
import helium314.keyboard.settings.SettingsWithoutKey
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.SettingsDestination
|
||||
import helium314.keyboard.settings.preferences.SliderPreference
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.dialogs.TextInputDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import helium314.keyboard.settings.preferences.BackupRestorePreference
|
||||
import helium314.keyboard.settings.preferences.LayoutEditPreference
|
||||
import helium314.keyboard.settings.preferences.LoadGestureLibPreference
|
||||
|
||||
@Composable
|
||||
fun AdvancedSettingsScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val prefs = LocalContext.current.prefs()
|
||||
val items = listOf(
|
||||
Settings.PREF_ALWAYS_INCOGNITO_MODE,
|
||||
Settings.PREF_KEY_LONGPRESS_TIMEOUT,
|
||||
Settings.PREF_SPACE_HORIZONTAL_SWIPE,
|
||||
Settings.PREF_SPACE_VERTICAL_SWIPE,
|
||||
if (Settings.readHorizontalSpaceSwipe(prefs) == KeyboardActionListener.SWIPE_SWITCH_LANGUAGE
|
||||
|| Settings.readVerticalSpaceSwipe(prefs) == KeyboardActionListener.SWIPE_SWITCH_LANGUAGE)
|
||||
Settings.PREF_LANGUAGE_SWIPE_DISTANCE else null,
|
||||
Settings.PREF_DELETE_SWIPE,
|
||||
Settings.PREF_SPACE_TO_CHANGE_LANG,
|
||||
Settings.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD,
|
||||
Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY,
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) Settings.PREF_SHOW_SETUP_WIZARD_ICON else null,
|
||||
Settings.PREF_ABC_AFTER_SYMBOL_SPACE,
|
||||
Settings.PREF_ABC_AFTER_EMOJI,
|
||||
Settings.PREF_ABC_AFTER_CLIP,
|
||||
Settings.PREF_CUSTOM_CURRENCY_KEY,
|
||||
Settings.PREF_MORE_POPUP_KEYS,
|
||||
SettingsWithoutKey.CUSTOM_SYMBOLS_NUMBER_LAYOUTS,
|
||||
SettingsWithoutKey.CUSTOM_FUNCTIONAL_LAYOUTS,
|
||||
SettingsWithoutKey.BACKUP_RESTORE,
|
||||
if (BuildConfig.DEBUG || prefs.getBoolean(DebugSettings.PREF_SHOW_DEBUG_SETTINGS, false)) SettingsWithoutKey.DEBUG_SETTINGS else null,
|
||||
R.string.settings_category_experimental,
|
||||
Settings.PREF_EMOJI_MAX_SDK,
|
||||
Settings.PREF_URL_DETECTION,
|
||||
if (BuildConfig.BUILD_TYPE != "nouserlib") SettingsWithoutKey.LOAD_GESTURE_LIB else null
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_advanced),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun createAdvancedSettings(context: Context) = listOf(
|
||||
Setting(context, Settings.PREF_ALWAYS_INCOGNITO_MODE,
|
||||
R.string.incognito, R.string.prefs_force_incognito_mode_summary)
|
||||
{
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_KEY_LONGPRESS_TIMEOUT, R.string.prefs_key_longpress_timeout_settings) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 300,
|
||||
range = 100f..700f,
|
||||
description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) }
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_SPACE_HORIZONTAL_SWIPE, R.string.show_horizontal_space_swipe) {
|
||||
val items = listOf(
|
||||
stringResource(R.string.space_swipe_move_cursor_entry) to "move_cursor",
|
||||
stringResource(R.string.switch_language) to "switch_language",
|
||||
stringResource(R.string.space_swipe_toggle_numpad_entry) to "toggle_numpad",
|
||||
stringResource(R.string.action_none) to "none",
|
||||
)
|
||||
ListPreference(it, items, "move_cursor")
|
||||
},
|
||||
Setting(context, Settings.PREF_SPACE_VERTICAL_SWIPE, R.string.show_vertical_space_swipe) {
|
||||
val items = listOf(
|
||||
stringResource(R.string.space_swipe_move_cursor_entry) to "move_cursor",
|
||||
stringResource(R.string.switch_language) to "switch_language",
|
||||
stringResource(R.string.space_swipe_toggle_numpad_entry) to "toggle_numpad",
|
||||
stringResource(R.string.action_none) to "none",
|
||||
)
|
||||
ListPreference(it, items, "none")
|
||||
},
|
||||
Setting(context, Settings.PREF_LANGUAGE_SWIPE_DISTANCE, R.string.prefs_language_swipe_distance) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 5,
|
||||
range = 2f..18f,
|
||||
description = { it.toString() }
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_DELETE_SWIPE, R.string.delete_swipe, R.string.delete_swipe_summary) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_SPACE_TO_CHANGE_LANG,
|
||||
R.string.prefs_long_press_keyboard_to_change_lang,
|
||||
R.string.prefs_long_press_keyboard_to_change_lang_summary)
|
||||
{
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD, R.string.prefs_long_press_symbol_for_numpad) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY, R.string.prefs_enable_emoji_alt_physical_key,
|
||||
R.string.prefs_enable_emoji_alt_physical_key_summary)
|
||||
{
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_SETUP_WIZARD_ICON, R.string.prefs_enable_emoji_alt_physical_key_summary) {
|
||||
val ctx = LocalContext.current
|
||||
SwitchPreference(it, true) { SystemBroadcastReceiver.toggleAppIcon(ctx) }
|
||||
},
|
||||
Setting(context, Settings.PREF_ABC_AFTER_SYMBOL_SPACE,
|
||||
R.string.switch_keyboard_after, R.string.after_symbol_and_space)
|
||||
{
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_ABC_AFTER_EMOJI, R.string.switch_keyboard_after, R.string.after_emoji) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_ABC_AFTER_CLIP, R.string.switch_keyboard_after, R.string.after_clip) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_CUSTOM_CURRENCY_KEY, R.string.customize_currencies) { setting ->
|
||||
var showDialog by remember { mutableStateOf(false) } // todo: textInputDialog...
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
if (showDialog) {
|
||||
val prefs = LocalContext.current.prefs()
|
||||
TextInputDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
textInputLabel = { Text(stringResource(R.string.customize_currencies_detail)) },
|
||||
initialText = prefs.getString(setting.key, "")!!,
|
||||
onConfirmed = { prefs.edit().putString(setting.key, it).apply(); KeyboardLayoutSet.onSystemLocaleChanged() },
|
||||
title = { Text(stringResource(R.string.customize_currencies)) },
|
||||
neutralButtonText = if (prefs.contains(setting.key)) stringResource(R.string.button_default) else null,
|
||||
onNeutral = { prefs.edit().remove(setting.key).apply(); KeyboardLayoutSet.onSystemLocaleChanged() },
|
||||
checkTextValid = { text -> text.splitOnWhitespace().none { it.length > 8 } }
|
||||
)
|
||||
}
|
||||
},
|
||||
Setting(context, Settings.PREF_MORE_POPUP_KEYS, R.string.show_popup_keys_title) {
|
||||
val items = listOf(
|
||||
stringResource(R.string.show_popup_keys_normal) to "normal",
|
||||
stringResource(R.string.show_popup_keys_main) to "main",
|
||||
stringResource(R.string.show_popup_keys_more) to "more",
|
||||
stringResource(R.string.show_popup_keys_all) to "all",
|
||||
)
|
||||
ListPreference(it, items, "main") { KeyboardLayoutSet.onSystemLocaleChanged() }
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.CUSTOM_SYMBOLS_NUMBER_LAYOUTS, R.string.customize_symbols_number_layouts) { setting ->
|
||||
LayoutEditPreference(
|
||||
setting = setting,
|
||||
items = RawKeyboardParser.symbolAndNumberLayouts,
|
||||
getItemName = { it.getStringResourceOrName("layout_", LocalContext.current) },
|
||||
getDefaultLayout = { LocalContext.current.assets.list("layouts")?.firstOrNull { it.startsWith("$it.") } }
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.CUSTOM_FUNCTIONAL_LAYOUTS, R.string.customize_functional_key_layouts) { setting ->
|
||||
LayoutEditPreference(
|
||||
setting = setting,
|
||||
items = listOf(CUSTOM_FUNCTIONAL_LAYOUT_NORMAL, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS, CUSTOM_FUNCTIONAL_LAYOUT_SYMBOLS_SHIFTED)
|
||||
.map { it.substringBeforeLast(".") },
|
||||
getItemName = { it.substringAfter(CUSTOM_LAYOUT_PREFIX).getStringResourceOrName("layout_", LocalContext.current) },
|
||||
getDefaultLayout = { if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json" }
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.BACKUP_RESTORE, R.string.backup_restore_title) {
|
||||
BackupRestorePreference(it)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.DEBUG_SETTINGS, R.string.debug_settings_title) {
|
||||
Preference(
|
||||
name = it.title,
|
||||
onClick = { SettingsDestination.navigateTo(SettingsDestination.Debug) }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
Setting(context, Settings.PREF_EMOJI_MAX_SDK, R.string.prefs_key_emoji_max_sdk) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = Build.VERSION.SDK_INT,
|
||||
range = 21f..35f,
|
||||
description = {
|
||||
"Android " + when(it) {
|
||||
21 -> "5.0"
|
||||
22 -> "5.1"
|
||||
23 -> "6"
|
||||
24 -> "7.0"
|
||||
25 -> "7.1"
|
||||
26 -> "8.0"
|
||||
27 -> "8.1"
|
||||
28 -> "9"
|
||||
29 -> "10"
|
||||
30 -> "11"
|
||||
31 -> "12"
|
||||
32 -> "12L"
|
||||
33 -> "13"
|
||||
34 -> "14"
|
||||
35 -> "15"
|
||||
else -> "version unknown"
|
||||
}
|
||||
},
|
||||
onValueChanged = { keyboardNeedsReload = true }
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_URL_DETECTION, R.string.url_detection_title, R.string.url_detection_summary) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.LOAD_GESTURE_LIB, R.string.load_gesture_library, R.string.load_gesture_library_summary) {
|
||||
LoadGestureLibPreference(it)
|
||||
},
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
AdvancedSettingsScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,340 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.compose.ui.tooling.preview.Preview
|
||||
import helium314.keyboard.keyboard.KeyboardLayoutSet
|
||||
import helium314.keyboard.keyboard.KeyboardSwitcher
|
||||
import helium314.keyboard.keyboard.KeyboardTheme
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.ColorsNightSettingsFragment
|
||||
import helium314.keyboard.latin.settings.ColorsSettingsFragment
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.settings.SettingsValues
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.getStringResourceOrName
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.latin.utils.switchTo
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.preferences.ListPreference
|
||||
import helium314.keyboard.settings.SettingsWithoutKey
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.preferences.SliderPreference
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.dialogs.CustomizeIconsDialog
|
||||
import helium314.keyboard.settings.dialogs.TextInputDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import helium314.keyboard.settings.preferences.BackgroundImagePref
|
||||
import helium314.keyboard.settings.preferences.CustomFontPreference
|
||||
|
||||
@Composable
|
||||
fun AppearanceScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
val b = (LocalContext.current.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val dayNightMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && Settings.readDayNightPref(prefs, ctx.resources)
|
||||
val items = listOf(
|
||||
R.string.settings_screen_theme,
|
||||
Settings.PREF_THEME_STYLE,
|
||||
Settings.PREF_ICON_STYLE,
|
||||
Settings.PREF_CUSTOM_ICON_NAMES,
|
||||
Settings.PREF_THEME_COLORS,
|
||||
if (prefs.getString(Settings.PREF_THEME_COLORS, KeyboardTheme.THEME_LIGHT) == KeyboardTheme.THEME_USER)
|
||||
SettingsWithoutKey.ADJUST_COLORS else null,
|
||||
Settings.PREF_THEME_KEY_BORDERS,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
Settings.PREF_THEME_DAY_NIGHT else null,
|
||||
if (dayNightMode) Settings.PREF_THEME_COLORS_NIGHT else null,
|
||||
if (dayNightMode && prefs.getString(Settings.PREF_THEME_COLORS_NIGHT, KeyboardTheme.THEME_DARK) == KeyboardTheme.THEME_USER_NIGHT)
|
||||
SettingsWithoutKey.ADJUST_COLORS_NIGHT else null,
|
||||
Settings.PREF_NAVBAR_COLOR,
|
||||
SettingsWithoutKey.BACKGROUND_IMAGE,
|
||||
SettingsWithoutKey.BACKGROUND_IMAGE_LANDSCAPE,
|
||||
R.string.settings_category_miscellaneous,
|
||||
Settings.PREF_ENABLE_SPLIT_KEYBOARD,
|
||||
if (prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD, false))
|
||||
Settings.PREF_SPLIT_SPACER_SCALE else null,
|
||||
Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE,
|
||||
if (prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE, false))
|
||||
Settings.PREF_SPLIT_SPACER_SCALE_LANDSCAPE else null,
|
||||
Settings.PREF_NARROW_KEY_GAPS,
|
||||
Settings.PREF_KEYBOARD_HEIGHT_SCALE,
|
||||
Settings.PREF_BOTTOM_PADDING_SCALE,
|
||||
Settings.PREF_BOTTOM_PADDING_SCALE_LANDSCAPE,
|
||||
Settings.PREF_SIDE_PADDING_SCALE,
|
||||
Settings.PREF_SIDE_PADDING_SCALE_LANDSCAPE,
|
||||
Settings.PREF_SPACE_BAR_TEXT,
|
||||
SettingsWithoutKey.CUSTOM_FONT,
|
||||
Settings.PREF_FONT_SCALE,
|
||||
Settings.PREF_EMOJI_FONT_SCALE,
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_appearance),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
fun createAppearanceSettings(context: Context) = listOf(
|
||||
Setting(context, Settings.PREF_THEME_STYLE, R.string.theme_style) { setting ->
|
||||
val ctx = LocalContext.current
|
||||
val prefs = ctx.prefs()
|
||||
val items = KeyboardTheme.STYLES.map {
|
||||
it.getStringResourceOrName("style_name_", ctx) to it
|
||||
}
|
||||
ListPreference(
|
||||
setting,
|
||||
items,
|
||||
KeyboardTheme.STYLE_MATERIAL
|
||||
) {
|
||||
if (it != KeyboardTheme.STYLE_HOLO) {
|
||||
// todo (later): use defaults once they exist
|
||||
if (prefs.getString(Settings.PREF_THEME_COLORS, "") == KeyboardTheme.THEME_HOLO_WHITE)
|
||||
prefs.edit().putString(Settings.PREF_THEME_COLORS, KeyboardTheme.THEME_LIGHT).apply()
|
||||
if (prefs.getString(Settings.PREF_THEME_COLORS_NIGHT, "") == KeyboardTheme.THEME_HOLO_WHITE)
|
||||
prefs.edit().putString(Settings.PREF_THEME_COLORS_NIGHT, KeyboardTheme.THEME_DARK).apply()
|
||||
}
|
||||
}
|
||||
},
|
||||
Setting(context, Settings.PREF_ICON_STYLE, R.string.icon_style) { setting ->
|
||||
val ctx = LocalContext.current
|
||||
val items = KeyboardTheme.STYLES.map { it.getStringResourceOrName("style_name_", ctx) to it }
|
||||
ListPreference(
|
||||
setting,
|
||||
items,
|
||||
KeyboardTheme.STYLE_MATERIAL
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_CUSTOM_ICON_NAMES, R.string.customize_icons) { setting ->
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
if (showDialog) {
|
||||
if (keyboardNeedsReload) {
|
||||
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(LocalContext.current)
|
||||
keyboardNeedsReload = false
|
||||
}
|
||||
CustomizeIconsDialog(setting.key) { showDialog = false }
|
||||
}
|
||||
},
|
||||
Setting(context, Settings.PREF_THEME_COLORS, R.string.theme_colors) { setting ->
|
||||
val ctx = LocalContext.current
|
||||
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val currentStyle = ctx.prefs().getString(Settings.PREF_THEME_STYLE, KeyboardTheme.STYLE_MATERIAL)
|
||||
val items = KeyboardTheme.COLORS.mapNotNull {
|
||||
if (it == KeyboardTheme.THEME_HOLO_WHITE && currentStyle != KeyboardTheme.STYLE_HOLO)
|
||||
return@mapNotNull null
|
||||
it.getStringResourceOrName("theme_name_", ctx) to it
|
||||
}
|
||||
ListPreference(
|
||||
setting,
|
||||
items,
|
||||
KeyboardTheme.THEME_LIGHT
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_THEME_COLORS_NIGHT, R.string.theme_colors_night) { setting ->
|
||||
val ctx = LocalContext.current
|
||||
val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val currentStyle = ctx.prefs().getString(Settings.PREF_THEME_STYLE, KeyboardTheme.STYLE_MATERIAL)
|
||||
val items = KeyboardTheme.COLORS_DARK.mapNotNull {
|
||||
if (it == KeyboardTheme.THEME_HOLO_WHITE && currentStyle == KeyboardTheme.STYLE_HOLO)
|
||||
return@mapNotNull null
|
||||
it.getStringResourceOrName("theme_name_", ctx) to it
|
||||
}
|
||||
ListPreference(
|
||||
setting,
|
||||
items,
|
||||
KeyboardTheme.THEME_DARK
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.ADJUST_COLORS, R.string.select_user_colors, R.string.select_user_colors_summary) {
|
||||
val ctx = LocalContext.current
|
||||
Preference(
|
||||
name = it.title,
|
||||
description = it.description,
|
||||
onClick = {
|
||||
ctx.getActivity()?.switchTo(ColorsSettingsFragment())
|
||||
//SettingsDestination.navigateTo(SettingsDestination.Colors) todo: later
|
||||
}
|
||||
)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.ADJUST_COLORS_NIGHT, R.string.select_user_colors_night, R.string.select_user_colors_summary) {
|
||||
val ctx = LocalContext.current
|
||||
Preference(
|
||||
name = it.title,
|
||||
description = it.description,
|
||||
onClick = {
|
||||
ctx.getActivity()?.switchTo(ColorsNightSettingsFragment())
|
||||
//SettingsDestination.navigateTo(SettingsDestination.ColorsNight) todo: later
|
||||
}
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_THEME_KEY_BORDERS, R.string.key_borders) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_THEME_DAY_NIGHT, R.string.day_night_mode, R.string.day_night_mode_summary) {
|
||||
SwitchPreference(it, Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_NAVBAR_COLOR, R.string.theme_navbar, R.string.day_night_mode_summary) {
|
||||
SwitchPreference(it, Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.BACKGROUND_IMAGE, R.string.customize_background_image) {
|
||||
BackgroundImagePref(it, false)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.BACKGROUND_IMAGE_LANDSCAPE,
|
||||
R.string.customize_background_image_landscape, R.string.summary_customize_background_image_landscape)
|
||||
{
|
||||
BackgroundImagePref(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_ENABLE_SPLIT_KEYBOARD, R.string.enable_split_keyboard) {
|
||||
SwitchPreference(it, false) { KeyboardSwitcher.getInstance().reloadKeyboard() }
|
||||
},
|
||||
Setting(context, Settings.PREF_SPLIT_SPACER_SCALE, R.string.split_spacer_scale) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = SettingsValues.DEFAULT_SIZE_SCALE,
|
||||
range = 0.5f..2f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE, R.string.enable_split_keyboard_landscape) {
|
||||
SwitchPreference(it, false) { KeyboardSwitcher.getInstance().reloadKeyboard() }
|
||||
},
|
||||
Setting(context, Settings.PREF_SPLIT_SPACER_SCALE_LANDSCAPE, R.string.split_spacer_scale_landscape) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = SettingsValues.DEFAULT_SIZE_SCALE,
|
||||
range = 0.5f..2f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_NARROW_KEY_GAPS, R.string.prefs_narrow_key_gaps) {
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_KEYBOARD_HEIGHT_SCALE, R.string.prefs_keyboard_height_scale) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = SettingsValues.DEFAULT_SIZE_SCALE,
|
||||
range = 0.5f..1.5f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_BOTTOM_PADDING_SCALE, R.string.prefs_bottom_padding_scale) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = SettingsValues.DEFAULT_SIZE_SCALE,
|
||||
range = 0f..5f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_BOTTOM_PADDING_SCALE_LANDSCAPE, R.string.prefs_bottom_padding_scale_landscape) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 0f,
|
||||
range = 0f..5f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_SIDE_PADDING_SCALE, R.string.prefs_side_padding_scale) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 0f,
|
||||
range = 0f..3f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_SIDE_PADDING_SCALE_LANDSCAPE, R.string.prefs_side_padding_scale_landscape) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 0f,
|
||||
range = 0f..3f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_SPACE_BAR_TEXT, R.string.prefs_space_bar_text) { setting ->
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) } // todo: textInputDialog...
|
||||
val prefs = LocalContext.current.prefs()
|
||||
Preference(
|
||||
name = setting.title,
|
||||
onClick = { showDialog = true },
|
||||
description = prefs.getString(setting.key, "")
|
||||
)
|
||||
if (showDialog) {
|
||||
TextInputDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
onConfirmed = {
|
||||
prefs.edit().putString(setting.key, it).apply()
|
||||
keyboardNeedsReload = true
|
||||
},
|
||||
initialText = prefs.getString(setting.key, "") ?: "",
|
||||
title = { Text(setting.title) },
|
||||
checkTextValid = { true }
|
||||
)
|
||||
}
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.CUSTOM_FONT, R.string.custom_font) {
|
||||
CustomFontPreference(it)
|
||||
},
|
||||
Setting(context, Settings.PREF_FONT_SCALE, R.string.prefs_font_scale) { def ->
|
||||
SliderPreference(
|
||||
name = def.title,
|
||||
key = def.key,
|
||||
default = 1f,
|
||||
range = 0.5f..1.5f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_EMOJI_FONT_SCALE, R.string.prefs_emoji_font_scale) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 1f,
|
||||
range = 0.5f..1.5f,
|
||||
description = { "${(100 * it).toInt()}%" }
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
AppearanceScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.settings.SearchScreen
|
||||
|
||||
@Composable
|
||||
fun ColorsScreen(
|
||||
night: Boolean,
|
||||
onClickBack: () -> Unit
|
||||
) {
|
||||
|
||||
var availableColors by remember { mutableStateOf(emptyList<ColorSetting>()) } // todo (later): type?
|
||||
// todo (later): save / load / type selection here? or in ... menu as previously?
|
||||
SearchScreen(
|
||||
title = stringResource(if (night) R.string.select_user_colors_night else R.string.select_user_colors),
|
||||
onClickBack = onClickBack,
|
||||
filteredItems = { search -> availableColors.filter { it.displayName.contains(search, true) } },
|
||||
itemContent = { }
|
||||
)
|
||||
}
|
||||
|
||||
private class ColorSetting(
|
||||
val key: String, // old, this should go away
|
||||
val displayName: String,
|
||||
var auto: Boolean, // not for all
|
||||
var color: Int
|
||||
)
|
|
@ -0,0 +1,119 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.BuildConfig
|
||||
import helium314.keyboard.latin.DictionaryDumpBroadcastReceiver
|
||||
import helium314.keyboard.latin.DictionaryFacilitator
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.DebugSettings
|
||||
import helium314.keyboard.latin.settings.DebugSettingsFragment
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
import helium314.keyboard.settings.preferences.PreferenceCategory
|
||||
|
||||
@Composable
|
||||
fun DebugScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
val settings = createDebugSettings(ctx)
|
||||
val items = listOfNotNull(
|
||||
if (!BuildConfig.DEBUG) DebugSettings.PREF_SHOW_DEBUG_SETTINGS else null,
|
||||
DebugSettings.PREF_DEBUG_MODE,
|
||||
DebugSettings.PREF_SHOW_SUGGESTION_INFOS,
|
||||
DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH,
|
||||
DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW,
|
||||
R.string.prefs_dump_dynamic_dicts
|
||||
) + DictionaryFacilitator.DYNAMIC_DICTIONARY_TYPES.map { DebugSettingsFragment.PREF_KEY_DUMP_DICT_PREFIX + it }
|
||||
SearchSettingsScreen(
|
||||
onClickBack = {
|
||||
if (needsRestart) {
|
||||
val intent = Intent.makeRestartActivityTask(ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)?.component)
|
||||
intent.setPackage(ctx.packageName)
|
||||
ctx.startActivity(intent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
onClickBack()
|
||||
},
|
||||
title = stringResource(R.string.debug_settings_title),
|
||||
settings = emptyList()
|
||||
) {
|
||||
// the preferences are not in SettingsContainer, so set content instead
|
||||
LazyColumn {
|
||||
items(items, key = { it }) { item ->
|
||||
if (item is Int) PreferenceCategory(stringResource(item))
|
||||
else settings.first { it.key == item }.Preference()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var needsRestart = false
|
||||
|
||||
private fun createDebugSettings(context: Context) = listOf(
|
||||
Setting(context, DebugSettings.PREF_SHOW_DEBUG_SETTINGS, R.string.prefs_show_debug_settings) { setting ->
|
||||
val prefs = LocalContext.current.prefs()
|
||||
SwitchPreference(setting, false)
|
||||
{ if (!it) prefs.edit().putBoolean(DebugSettings.PREF_DEBUG_MODE, false).apply() }
|
||||
},
|
||||
Setting(context, DebugSettings.PREF_DEBUG_MODE, R.string.prefs_debug_mode) { setting ->
|
||||
val prefs = LocalContext.current.prefs()
|
||||
SwitchPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
description = stringResource(R.string.version_text, BuildConfig.VERSION_NAME),
|
||||
default = false,
|
||||
) {
|
||||
if (!it) prefs.edit().putBoolean(DebugSettings.PREF_SHOW_SUGGESTION_INFOS, false).apply()
|
||||
needsRestart = true
|
||||
}
|
||||
},
|
||||
Setting(context, DebugSettings.PREF_SHOW_SUGGESTION_INFOS, R.string.prefs_show_suggestion_infos) {
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, R.string.prefs_force_non_distinct_multitouch) {
|
||||
SwitchPreference(it, false) { needsRestart = true }
|
||||
},
|
||||
Setting(context, DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, R.string.sliding_key_input_preview, R.string.sliding_key_input_preview_summary) { def ->
|
||||
SwitchPreference(def, false)
|
||||
},
|
||||
) + DictionaryFacilitator.DYNAMIC_DICTIONARY_TYPES.map { type ->
|
||||
Setting(context, DebugSettingsFragment.PREF_KEY_DUMP_DICT_PREFIX + type, R.string.button_default) {
|
||||
val ctx = LocalContext.current
|
||||
Preference(
|
||||
name = "Dump $type dictionary",
|
||||
onClick = {
|
||||
val intent = Intent(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION)
|
||||
intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, type)
|
||||
ctx.sendBroadcast(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
DebugScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.preferences.SliderPreference
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
|
||||
@Composable
|
||||
fun GestureTypingScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val prefs = LocalContext.current.prefs()
|
||||
val b = (LocalContext.current.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val gestureFloatingPreviewEnabled = prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true)
|
||||
val gestureEnabled = prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true)
|
||||
val items = listOf(
|
||||
Settings.PREF_GESTURE_INPUT,
|
||||
if (gestureEnabled)
|
||||
Settings.PREF_GESTURE_PREVIEW_TRAIL else null,
|
||||
if (gestureEnabled)
|
||||
Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT else null,
|
||||
if (gestureEnabled && gestureFloatingPreviewEnabled)
|
||||
Settings.PREF_GESTURE_FLOATING_PREVIEW_DYNAMIC else null,
|
||||
if (gestureEnabled)
|
||||
Settings.PREF_GESTURE_SPACE_AWARE else null,
|
||||
if (gestureEnabled)
|
||||
Settings.PREF_GESTURE_FAST_TYPING_COOLDOWN else null,
|
||||
if (gestureEnabled && (prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true) || gestureFloatingPreviewEnabled))
|
||||
Settings.PREF_GESTURE_TRAIL_FADEOUT_DURATION else null
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_gesture),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
fun createGestureTypingSettings(context: Context) = listOf(
|
||||
Setting(context, Settings.PREF_GESTURE_INPUT, R.string.gesture_input, R.string.gesture_input_summary) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_GESTURE_PREVIEW_TRAIL, R.string.gesture_preview_trail) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT,
|
||||
R.string.gesture_floating_preview_static, R.string.gesture_floating_preview_static_summary)
|
||||
{
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_GESTURE_FLOATING_PREVIEW_DYNAMIC,
|
||||
R.string.gesture_floating_preview_text, R.string.gesture_floating_preview_dynamic_summary)
|
||||
{ def ->
|
||||
val ctx = LocalContext.current
|
||||
SwitchPreference(def, true) {
|
||||
// is this complexity and 2 pref keys for one setting really needed?
|
||||
// default value is based on system reduced motion
|
||||
val default = Settings.readGestureDynamicPreviewDefault(ctx)
|
||||
val followingSystem = it == default
|
||||
// allow the default to be overridden
|
||||
ctx.prefs().edit().putBoolean(Settings.PREF_GESTURE_DYNAMIC_PREVIEW_FOLLOW_SYSTEM, followingSystem).apply()
|
||||
keyboardNeedsReload = true
|
||||
}
|
||||
},
|
||||
Setting(context, Settings.PREF_GESTURE_SPACE_AWARE, R.string.gesture_space_aware, R.string.gesture_space_aware_summary) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_GESTURE_FAST_TYPING_COOLDOWN, R.string.gesture_fast_typing_cooldown) { def ->
|
||||
SliderPreference(
|
||||
name = def.title,
|
||||
key = def.key,
|
||||
default = 500,
|
||||
range = 0f..500f,
|
||||
description = {
|
||||
if (it <= 0) stringResource(R.string.gesture_fast_typing_cooldown_instant)
|
||||
else stringResource(R.string.abbreviation_unit_milliseconds, it.toString())
|
||||
}
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_GESTURE_TRAIL_FADEOUT_DURATION, R.string.gesture_trail_fadeout_duration) { def ->
|
||||
SliderPreference(
|
||||
name = def.title,
|
||||
key = def.key,
|
||||
default = 800,
|
||||
range = 100f..1900f,
|
||||
description = { stringResource(R.string.abbreviation_unit_milliseconds, (it + 100).toString()) },
|
||||
stepSize = 10,
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
GestureTypingScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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 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
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.switchTo
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.preferences.PreferenceCategory
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.Theme
|
||||
|
||||
@Composable
|
||||
fun MainSettingsScreen(
|
||||
onClickAbout: () -> Unit,
|
||||
onClickTextCorrection: () -> Unit,
|
||||
onClickPreferences: () -> Unit,
|
||||
onClickToolbar: () -> Unit,
|
||||
onClickGestureTyping: () -> Unit,
|
||||
onClickAdvanced: () -> Unit,
|
||||
onClickAppearance: () -> Unit,
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.ime_settings),
|
||||
settings = emptyList(),
|
||||
) {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Preference(
|
||||
name = stringResource(R.string.settings_screen_preferences),
|
||||
onClick = onClickPreferences,
|
||||
icon = R.drawable.ic_settings_preferences_foreground
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
name = stringResource(R.string.settings_screen_appearance),
|
||||
onClick = onClickAppearance,
|
||||
icon = R.drawable.ic_settings_appearance_foreground
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
name = stringResource(R.string.settings_screen_toolbar),
|
||||
onClick = onClickToolbar,
|
||||
icon = R.drawable.ic_settings_toolbar_foreground
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
if (JniUtils.sHaveGestureLib)
|
||||
Preference(
|
||||
name = stringResource(R.string.settings_screen_gesture),
|
||||
onClick = onClickGestureTyping,
|
||||
icon = R.drawable.ic_settings_gesture_foreground
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
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),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
name = stringResource(R.string.settings_screen_advanced),
|
||||
onClick = onClickAdvanced,
|
||||
icon = R.drawable.ic_settings_advanced_foreground
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
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()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewScreen() {
|
||||
Theme(true) {
|
||||
Surface {
|
||||
MainSettingsScreen({}, {}, {}, {}, {}, {}, {}, {})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import helium314.keyboard.keyboard.KeyboardLayoutSet
|
||||
import helium314.keyboard.latin.AudioAndHapticFeedbackManager
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.POPUP_KEYS_LABEL_DEFAULT
|
||||
import helium314.keyboard.latin.utils.POPUP_KEYS_ORDER_DEFAULT
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.getEnabledSubtypes
|
||||
import helium314.keyboard.latin.utils.locale
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.preferences.ListPreference
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.ReorderSwitchPreference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.preferences.SliderPreference
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
|
||||
@Composable
|
||||
fun PreferencesScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val prefs = LocalContext.current.prefs()
|
||||
val b = (LocalContext.current.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val items = listOf(
|
||||
R.string.settings_category_input,
|
||||
Settings.PREF_SHOW_HINTS,
|
||||
if (prefs.getBoolean(Settings.PREF_SHOW_HINTS, true))
|
||||
Settings.PREF_POPUP_KEYS_LABELS_ORDER else null,
|
||||
Settings.PREF_POPUP_KEYS_ORDER,
|
||||
Settings.PREF_SHOW_POPUP_HINTS,
|
||||
Settings.PREF_POPUP_ON,
|
||||
Settings.PREF_VIBRATE_ON,
|
||||
if (prefs.getBoolean(Settings.PREF_VIBRATE_ON, true))
|
||||
Settings.PREF_VIBRATION_DURATION_SETTINGS else null,
|
||||
if (prefs.getBoolean(Settings.PREF_VIBRATE_ON, true))
|
||||
Settings.PREF_VIBRATE_IN_DND_MODE else null,
|
||||
Settings.PREF_SOUND_ON,
|
||||
if (prefs.getBoolean(Settings.PREF_SOUND_ON, true))
|
||||
Settings.PREF_KEYPRESS_SOUND_VOLUME else null,
|
||||
R.string.settings_category_additional_keys,
|
||||
Settings.PREF_SHOW_NUMBER_ROW,
|
||||
if (getEnabledSubtypes(prefs, true).any { it.locale().language in localesWithLocalizedNumberRow })
|
||||
Settings.PREF_LOCALIZED_NUMBER_ROW else null,
|
||||
if (prefs.getBoolean(Settings.PREF_SHOW_HINTS, true) && prefs.getBoolean(Settings.PREF_SHOW_NUMBER_ROW, false))
|
||||
Settings.PREF_SHOW_NUMBER_ROW_HINTS else null,
|
||||
Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY,
|
||||
Settings.PREF_LANGUAGE_SWITCH_KEY,
|
||||
Settings.PREF_SHOW_EMOJI_KEY,
|
||||
Settings.PREF_REMOVE_REDUNDANT_POPUPS,
|
||||
R.string.settings_category_clipboard_history,
|
||||
Settings.PREF_ENABLE_CLIPBOARD_HISTORY,
|
||||
if (prefs.getBoolean(Settings.PREF_ENABLE_CLIPBOARD_HISTORY, true))
|
||||
Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME else null
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_preferences),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
fun createPreferencesSettings(context: Context) = listOf(
|
||||
Setting(context, Settings.PREF_SHOW_HINTS, R.string.show_hints, R.string.show_hints_summary) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_POPUP_KEYS_LABELS_ORDER, R.string.hint_source) {
|
||||
ReorderSwitchPreference(it, POPUP_KEYS_LABEL_DEFAULT)
|
||||
},
|
||||
Setting(context, Settings.PREF_POPUP_KEYS_ORDER, R.string.popup_order) {
|
||||
ReorderSwitchPreference(it, POPUP_KEYS_ORDER_DEFAULT)
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_POPUP_HINTS, R.string.show_popup_hints, R.string.show_popup_hints_summary) {
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_POPUP_ON, R.string.popup_on_keypress) {
|
||||
val dm = LocalContext.current.resources.displayMetrics
|
||||
val px600 = with(LocalDensity.current) { 600.dp.toPx() }
|
||||
SwitchPreference(it, dm.widthPixels >= px600 || dm.heightPixels >= px600)
|
||||
},
|
||||
Setting(context, Settings.PREF_VIBRATE_ON, R.string.vibrate_on_keypress) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_VIBRATE_IN_DND_MODE, R.string.vibrate_in_dnd_mode) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_SOUND_ON, R.string.sound_on_keypress) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_ENABLE_CLIPBOARD_HISTORY,
|
||||
R.string.enable_clipboard_history, R.string.enable_clipboard_history_summary)
|
||||
{
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_NUMBER_ROW, R.string.number_row, R.string.number_row_summary) {
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_LOCALIZED_NUMBER_ROW, R.string.localized_number_row, R.string.localized_number_row_summary) {
|
||||
SwitchPreference(it, true) { KeyboardLayoutSet.onSystemLocaleChanged() }
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_NUMBER_ROW_HINTS, R.string.number_row_hints) {
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, R.string.show_language_switch_key) {
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_LANGUAGE_SWITCH_KEY, R.string.language_switch_key_behavior) {
|
||||
ListPreference(
|
||||
it,
|
||||
listOf(
|
||||
"internal" to stringResource(R.string.switch_language),
|
||||
"input_method" to stringResource(R.string.language_switch_key_switch_input_method),
|
||||
"both" to stringResource(R.string.language_switch_key_switch_both)
|
||||
),
|
||||
"internal"
|
||||
) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_EMOJI_KEY, R.string.show_emoji_key) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_REMOVE_REDUNDANT_POPUPS,
|
||||
R.string.remove_redundant_popups, R.string.remove_redundant_popups_summary)
|
||||
{
|
||||
SwitchPreference(it, false) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, R.string.clipboard_history_retention_time) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = 10,
|
||||
description = {
|
||||
if (it < 0) stringResource(R.string.settings_no_limit)
|
||||
else stringResource(R.string.abbreviation_unit_minutes, it.toString())
|
||||
},
|
||||
range = -1f..120f,
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_VIBRATION_DURATION_SETTINGS, R.string.prefs_keypress_vibration_duration_settings) { setting ->
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = -1,
|
||||
description = {
|
||||
if (it < 0) stringResource(R.string.settings_system_default)
|
||||
else stringResource(R.string.abbreviation_unit_milliseconds, it.toString())
|
||||
},
|
||||
range = -1f..100f,
|
||||
onValueChanged = { AudioAndHapticFeedbackManager.getInstance().vibrate(it.toLong()) }
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_KEYPRESS_SOUND_VOLUME, R.string.prefs_keypress_sound_volume_settings) { setting ->
|
||||
val audioManager = LocalContext.current.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
SliderPreference(
|
||||
name = setting.title,
|
||||
key = setting.key,
|
||||
default = -0.01f,
|
||||
description = {
|
||||
if (it < 0) stringResource(R.string.settings_system_default)
|
||||
else (it * 100).toInt().toString()
|
||||
},
|
||||
range = -0.01f..1f,
|
||||
onValueChanged = { audioManager.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, it) }
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// todo (later): not good to have it hardcoded, but reading a bunch of files may be noticeably slow
|
||||
private val localesWithLocalizedNumberRow = listOf("ar", "bn", "fa", "gu", "hi", "kn", "mr", "ne", "ur")
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
PreferencesScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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 helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.permissions.PermissionsUtil
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.settings.UserDictionaryListFragment
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.getActivity
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.latin.utils.switchTo
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.preferences.ListPreference
|
||||
import helium314.keyboard.settings.SettingsWithoutKey
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.dialogs.ConfirmationDialog
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
|
||||
@Composable
|
||||
fun TextCorrectionScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val prefs = LocalContext.current.prefs()
|
||||
val b = (LocalContext.current.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
|
||||
if ((b?.value ?: 0) < 0)
|
||||
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
|
||||
val autocorrectEnabled = prefs.getBoolean(Settings.PREF_AUTO_CORRECTION, true)
|
||||
val suggestionsEnabled = prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, true)
|
||||
val items = listOf(
|
||||
SettingsWithoutKey.EDIT_PERSONAL_DICTIONARY,
|
||||
R.string.settings_category_correction,
|
||||
Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE,
|
||||
Settings.PREF_AUTO_CORRECTION,
|
||||
if (autocorrectEnabled) Settings.PREF_MORE_AUTO_CORRECTION else null,
|
||||
if (autocorrectEnabled) Settings.PREF_AUTOCORRECT_SHORTCUTS else null,
|
||||
if (autocorrectEnabled) Settings.PREF_AUTO_CORRECTION_CONFIDENCE else null,
|
||||
Settings.PREF_AUTO_CAP,
|
||||
Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD,
|
||||
Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION,
|
||||
R.string.settings_category_suggestions,
|
||||
Settings.PREF_SHOW_SUGGESTIONS,
|
||||
if (suggestionsEnabled) Settings.PREF_ALWAYS_SHOW_SUGGESTIONS else null,
|
||||
if (suggestionsEnabled) Settings.PREF_CENTER_SUGGESTION_TEXT_TO_ENTER else null,
|
||||
Settings.PREF_KEY_USE_PERSONALIZED_DICTS,
|
||||
Settings.PREF_BIGRAM_PREDICTIONS,
|
||||
Settings.PREF_SUGGEST_CLIPBOARD_CONTENT,
|
||||
Settings.PREF_USE_CONTACTS,
|
||||
if (prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true))
|
||||
Settings.PREF_ADD_TO_PERSONAL_DICTIONARY else null
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_correction),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
fun createCorrectionSettings(context: Context) = listOf(
|
||||
Setting(context, SettingsWithoutKey.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()) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_left),
|
||||
modifier = Modifier.scale(-1f, 1f),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
Setting(context, Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE,
|
||||
R.string.prefs_block_potentially_offensive_title, R.string.prefs_block_potentially_offensive_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTO_CORRECTION,
|
||||
R.string.autocorrect, R.string.auto_correction_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_MORE_AUTO_CORRECTION,
|
||||
R.string.more_autocorrect, R.string.more_autocorrect_summary
|
||||
) {
|
||||
SwitchPreference(it, true) // todo (later): shouldn't it better be false?
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTOCORRECT_SHORTCUTS,
|
||||
R.string.auto_correct_shortcuts, R.string.auto_correct_shortcuts_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTO_CORRECTION_CONFIDENCE, R.string.auto_correction_confidence) {
|
||||
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",
|
||||
)
|
||||
ListPreference(it, items, "0")
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTO_CAP,
|
||||
R.string.auto_cap, R.string.auto_cap_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD,
|
||||
R.string.use_double_space_period, R.string.use_double_space_period_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION,
|
||||
R.string.autospace_after_punctuation, R.string.autospace_after_punctuation_summary
|
||||
) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_SHOW_SUGGESTIONS,
|
||||
R.string.prefs_show_suggestions, R.string.prefs_show_suggestions_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_ALWAYS_SHOW_SUGGESTIONS,
|
||||
R.string.prefs_always_show_suggestions, R.string.prefs_always_show_suggestions_summary
|
||||
) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
Setting(context, Settings.PREF_KEY_USE_PERSONALIZED_DICTS,
|
||||
R.string.use_personalized_dicts, R.string.use_personalized_dicts_summary
|
||||
) { setting ->
|
||||
var showConfirmDialog by rememberSaveable { mutableStateOf(false) }
|
||||
SwitchPreference(setting, true,
|
||||
allowCheckedChange = {
|
||||
showConfirmDialog = !it
|
||||
it
|
||||
}
|
||||
)
|
||||
if (showConfirmDialog) {
|
||||
val prefs = LocalContext.current.prefs()
|
||||
ConfirmationDialog(
|
||||
onDismissRequest = { showConfirmDialog = false },
|
||||
onConfirmed = {
|
||||
prefs.edit().putBoolean(setting.key, false).apply()
|
||||
},
|
||||
text = { Text(stringResource(R.string.disable_personalized_dicts_message)) }
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
Setting(context, Settings.PREF_BIGRAM_PREDICTIONS,
|
||||
R.string.bigram_prediction, R.string.bigram_prediction_summary
|
||||
) {
|
||||
SwitchPreference(it, true) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(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)
|
||||
},
|
||||
Setting(context, Settings.PREF_SUGGEST_CLIPBOARD_CONTENT,
|
||||
R.string.suggest_clipboard_content, R.string.suggest_clipboard_content_summary
|
||||
) {
|
||||
SwitchPreference(it, true)
|
||||
},
|
||||
Setting(context, Settings.PREF_USE_CONTACTS,
|
||||
R.string.use_contacts_dict, R.string.use_contacts_dict_summary
|
||||
) { setting ->
|
||||
val activity = LocalContext.current.getActivity() ?: return@Setting
|
||||
var granted by remember { mutableStateOf(PermissionsUtil.checkAllPermissionsGranted(activity, Manifest.permission.READ_CONTACTS)) }
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
granted = it
|
||||
if (granted)
|
||||
activity.prefs().edit().putBoolean(setting.key, true).apply()
|
||||
}
|
||||
SwitchPreference(setting, false,
|
||||
allowCheckedChange = {
|
||||
if (it && !granted) {
|
||||
launcher.launch(Manifest.permission.READ_CONTACTS)
|
||||
false
|
||||
} else true
|
||||
}
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_ADD_TO_PERSONAL_DICTIONARY,
|
||||
R.string.add_to_personal_dictionary, R.string.add_to_personal_dictionary_summary
|
||||
) {
|
||||
SwitchPreference(it, false)
|
||||
},
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreferencePreview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
TextCorrectionScreen { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.settings.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.VectorDrawable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
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.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.util.TypedValueCompat
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.defaultClipboardToolbarPref
|
||||
import helium314.keyboard.latin.utils.defaultPinnedToolbarPref
|
||||
import helium314.keyboard.latin.utils.defaultToolbarPref
|
||||
import helium314.keyboard.settings.SettingsContainer
|
||||
import helium314.keyboard.settings.SettingsWithoutKey
|
||||
import helium314.keyboard.settings.Setting
|
||||
import helium314.keyboard.settings.preferences.Preference
|
||||
import helium314.keyboard.settings.preferences.ReorderSwitchPreference
|
||||
import helium314.keyboard.settings.SearchSettingsScreen
|
||||
import helium314.keyboard.settings.SettingsActivity
|
||||
import helium314.keyboard.settings.preferences.SwitchPreference
|
||||
import helium314.keyboard.settings.Theme
|
||||
import helium314.keyboard.settings.dialogs.ToolbarKeysCustomizer
|
||||
import helium314.keyboard.settings.keyboardNeedsReload
|
||||
|
||||
@Composable
|
||||
fun ToolbarScreen(
|
||||
onClickBack: () -> Unit,
|
||||
) {
|
||||
val items = listOf(
|
||||
Settings.PREF_TOOLBAR_KEYS,
|
||||
Settings.PREF_PINNED_TOOLBAR_KEYS,
|
||||
Settings.PREF_CLIPBOARD_TOOLBAR_KEYS,
|
||||
SettingsWithoutKey.CUSTOM_KEY_CODES,
|
||||
Settings.PREF_QUICK_PIN_TOOLBAR_KEYS,
|
||||
Settings.PREF_AUTO_SHOW_TOOLBAR,
|
||||
Settings.PREF_AUTO_HIDE_TOOLBAR,
|
||||
Settings.PREF_VARIABLE_TOOLBAR_DIRECTION
|
||||
)
|
||||
SearchSettingsScreen(
|
||||
onClickBack = onClickBack,
|
||||
title = stringResource(R.string.settings_screen_toolbar),
|
||||
settings = items
|
||||
)
|
||||
}
|
||||
|
||||
fun createToolbarSettings(context: Context) = listOf(
|
||||
Setting(context, Settings.PREF_TOOLBAR_KEYS, R.string.toolbar_keys) {
|
||||
ReorderSwitchPreference(it, defaultToolbarPref)
|
||||
},
|
||||
Setting(context, Settings.PREF_PINNED_TOOLBAR_KEYS, R.string.pinned_toolbar_keys) {
|
||||
ReorderSwitchPreference(it, defaultPinnedToolbarPref)
|
||||
},
|
||||
Setting(context, Settings.PREF_CLIPBOARD_TOOLBAR_KEYS, R.string.clipboard_toolbar_keys) {
|
||||
ReorderSwitchPreference(it, defaultClipboardToolbarPref)
|
||||
},
|
||||
Setting(context, SettingsWithoutKey.CUSTOM_KEY_CODES, R.string.customize_toolbar_key_codes) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
Preference(
|
||||
name = it.title,
|
||||
onClick = { showDialog = true },
|
||||
)
|
||||
if (showDialog)
|
||||
// todo (later): CUSTOM_KEY_CODES vs the 2 actual prefs that are changed...
|
||||
ToolbarKeysCustomizer(
|
||||
onDismissRequest = { showDialog = false }
|
||||
)
|
||||
},
|
||||
Setting(context, Settings.PREF_QUICK_PIN_TOOLBAR_KEYS,
|
||||
R.string.quick_pin_toolbar_keys, R.string.quick_pin_toolbar_keys_summary)
|
||||
{
|
||||
SwitchPreference(it, false,) { keyboardNeedsReload = true }
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTO_SHOW_TOOLBAR, R.string.auto_show_toolbar, R.string.auto_show_toolbar_summary)
|
||||
{
|
||||
SwitchPreference(it, false,)
|
||||
},
|
||||
Setting(context, Settings.PREF_AUTO_HIDE_TOOLBAR, R.string.auto_hide_toolbar, R.string.auto_hide_toolbar_summary)
|
||||
{
|
||||
SwitchPreference(it, false,)
|
||||
},
|
||||
Setting(context, Settings.PREF_VARIABLE_TOOLBAR_DIRECTION,
|
||||
R.string.var_toolbar_direction, R.string.var_toolbar_direction_summary)
|
||||
{
|
||||
SwitchPreference(it, true,)
|
||||
}
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SettingsActivity.settingsContainer = SettingsContainer(LocalContext.current)
|
||||
KeyboardIconsSet.instance.loadIcons(LocalContext.current)
|
||||
Theme(true) {
|
||||
Surface {
|
||||
ToolbarScreen { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyboardIconsSet.GetIcon(name: String?) {
|
||||
val ctx = LocalContext.current
|
||||
val drawable = getNewDrawable(name, ctx)
|
||||
Box(Modifier.size(40.dp), contentAlignment = Alignment.Center) {
|
||||
if (drawable is VectorDrawable)
|
||||
Icon(painterResource(iconIds[name?.lowercase()]!!), null, Modifier.fillMaxSize(0.8f))
|
||||
else if (drawable != null) {
|
||||
val px = TypedValueCompat.dpToPx(40f, ctx.resources.displayMetrics).toInt()
|
||||
Icon(drawable.toBitmap(px, px).asImageBitmap(), null, Modifier.fillMaxSize(0.8f))
|
||||
}
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/baseline_arrow_back_24.xml
Normal file
9
app/src/main/res/drawable/baseline_arrow_back_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<!--
|
||||
icon available in Android Studio
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
|
||||
</vector>
|
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">
|
||||
<style name="platformActivityTheme" parent="Theme.AppCompat.DayNight">
|
||||
<style name="platformActivityTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:colorAccent">@color/accent</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue