From fb967ea5bcb73888d8695cfad0033c9e1780380b Mon Sep 17 00:00:00 2001 From: lm41 Date: Mon, 16 Sep 2024 15:45:57 +0200 Subject: [PATCH] Implement internal text field highly inspired by #1897 --- .../florisboard/FlorisImeService.kt | 22 +- .../patrickgold/florisboard/app/AppPrefs.kt | 8 +- .../app/devtools/DevtoolsScreen.kt | 10 +- .../florisboard/ime/text/TextInputLayout.kt | 12 + .../lib/compose/FlorisInternalTextField.kt | 217 ++++++++++++++++++ 5 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 app/src/main/kotlin/dev/patrickgold/florisboard/lib/compose/FlorisInternalTextField.kt diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt index 8624026e..9a9f9f8a 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt @@ -105,13 +105,6 @@ import dev.patrickgold.florisboard.lib.devtools.flogError import dev.patrickgold.florisboard.lib.devtools.flogInfo import dev.patrickgold.florisboard.lib.devtools.flogWarning import dev.patrickgold.florisboard.lib.observeAsTransformingState -import org.florisboard.lib.snygg.ui.SnyggSurface -import org.florisboard.lib.snygg.ui.shape -import org.florisboard.lib.snygg.ui.snyggBackground -import org.florisboard.lib.snygg.ui.snyggBorder -import org.florisboard.lib.snygg.ui.snyggShadow -import org.florisboard.lib.snygg.ui.solidColor -import org.florisboard.lib.snygg.ui.spSize import dev.patrickgold.florisboard.lib.util.ViewUtils import dev.patrickgold.florisboard.lib.util.debugSummarize import dev.patrickgold.florisboard.lib.util.launchActivity @@ -123,6 +116,13 @@ import org.florisboard.lib.android.isOrientationPortrait import org.florisboard.lib.android.showShortToast import org.florisboard.lib.android.systemServiceOrNull import org.florisboard.lib.kotlin.collectLatestIn +import org.florisboard.lib.snygg.ui.SnyggSurface +import org.florisboard.lib.snygg.ui.shape +import org.florisboard.lib.snygg.ui.snyggBackground +import org.florisboard.lib.snygg.ui.snyggBorder +import org.florisboard.lib.snygg.ui.snyggShadow +import org.florisboard.lib.snygg.ui.solidColor +import org.florisboard.lib.snygg.ui.spSize import java.lang.ref.WeakReference /** @@ -143,8 +143,14 @@ class FlorisImeService : LifecycleInputMethodService() { private val InlineSuggestionUiSmallestSize = Size(0, 0) private val InlineSuggestionUiBiggestSize = Size(Int.MAX_VALUE, Int.MAX_VALUE) + private var CurrentInputConnection: InputConnection? = null + + fun setCurrentInputConnection(inputConnection: InputConnection?) { + CurrentInputConnection = inputConnection + } + fun currentInputConnection(): InputConnection? { - return FlorisImeServiceReference.get()?.currentInputConnection + return CurrentInputConnection ?: FlorisImeServiceReference.get()?.currentInputConnection } fun inputFeedbackController(): InputFeedbackController? { diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/app/AppPrefs.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/app/AppPrefs.kt index e8023671..bddd83a3 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/app/AppPrefs.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/app/AppPrefs.kt @@ -46,16 +46,16 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintMode import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction import dev.patrickgold.florisboard.ime.theme.ThemeMode import dev.patrickgold.florisboard.ime.theme.extCoreTheme -import org.florisboard.lib.android.isOrientationPortrait import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName import dev.patrickgold.florisboard.lib.observeAsTransformingState -import org.florisboard.lib.snygg.SnyggLevel import dev.patrickgold.florisboard.lib.util.VersionName import dev.patrickgold.jetpref.datastore.JetPref import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry import dev.patrickgold.jetpref.datastore.model.PreferenceModel import dev.patrickgold.jetpref.datastore.model.PreferenceType import dev.patrickgold.jetpref.datastore.model.observeAsState +import org.florisboard.lib.android.isOrientationPortrait +import org.florisboard.lib.snygg.SnyggLevel fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs) @@ -179,6 +179,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") { key = "devtools__show_drag_and_drop_helpers", default = false, ) + val enableInternalTextField = boolean( + key = "devtools__enable_internal_text_field", + default = false + ) } val dictionary = Dictionary() diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/app/devtools/DevtoolsScreen.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/app/devtools/DevtoolsScreen.kt index 0cedea21..5885a3e4 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/app/devtools/DevtoolsScreen.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/app/devtools/DevtoolsScreen.kt @@ -27,8 +27,6 @@ import dev.patrickgold.florisboard.app.Routes import dev.patrickgold.florisboard.extensionManager import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager import dev.patrickgold.florisboard.ime.dictionary.FlorisUserDictionaryDatabase -import org.florisboard.lib.android.AndroidSettings -import org.florisboard.lib.android.showLongToast import dev.patrickgold.florisboard.lib.compose.FlorisConfirmDeleteDialog import dev.patrickgold.florisboard.lib.compose.FlorisScreen import dev.patrickgold.florisboard.lib.compose.stringRes @@ -36,6 +34,8 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState import dev.patrickgold.jetpref.datastore.ui.Preference import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup import dev.patrickgold.jetpref.datastore.ui.SwitchPreference +import org.florisboard.lib.android.AndroidSettings +import org.florisboard.lib.android.showLongToast class DebugOnPurposeCrashException : Exception( "Success! The app crashed purposely to display this beautiful screen we all love :)" @@ -96,6 +96,12 @@ fun DevtoolsScreen() = FlorisScreen { summary = stringRes(R.string.devtools__show_drag_and_drop_helpers__summary), enabledIf = { prefs.devtools.enabled isEqualTo true }, ) + SwitchPreference( + prefs.devtools.enableInternalTextField, + title = "Enable internal text field", + summary = "Enable the internal text field for testing purposes until it's correctly implemented in the new emoji layout", + enabledIf = { prefs.devtools.enabled isEqualTo true} + ) Preference( title = stringRes(R.string.devtools__clear_udm_internal_database__label), summary = stringRes(R.string.devtools__clear_udm_internal_database__summary), diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt index 7519496d..1ce4b260 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt @@ -26,6 +26,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -43,6 +46,7 @@ import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyboardLayout import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme import dev.patrickgold.florisboard.ime.theme.FlorisImeUi import dev.patrickgold.florisboard.keyboardManager +import dev.patrickgold.florisboard.lib.compose.FlorisInternalTextField import dev.patrickgold.jetpref.datastore.model.observeAsState import org.florisboard.lib.snygg.ui.solidColor @@ -55,6 +59,8 @@ fun TextInputLayout( val prefs by florisPreferenceModel() + var internalText by rememberSaveable { mutableStateOf("") } + val state by keyboardManager.activeState.collectAsState() val evaluator by keyboardManager.activeEvaluator.collectAsState() @@ -64,6 +70,12 @@ fun TextInputLayout( .fillMaxWidth() .wrapContentHeight(), ) { + if (prefs.devtools.enableInternalTextField.observeAsState().value) { + FlorisInternalTextField( + value = internalText, + onValueChange = { internalText = it }, + ) + } Smartbar() if (state.isActionsOverflowVisible) { QuickActionsOverflowPanel() diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/lib/compose/FlorisInternalTextField.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/lib/compose/FlorisInternalTextField.kt new file mode 100644 index 00000000..a43f6b79 --- /dev/null +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/lib/compose/FlorisInternalTextField.kt @@ -0,0 +1,217 @@ +package dev.patrickgold.florisboard.lib.compose + +import android.graphics.Rect +import android.text.InputType +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import dev.patrickgold.florisboard.FlorisImeService +import dev.patrickgold.florisboard.lib.ValidationResult + +@Composable +fun FlorisInternalTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + textStyle: TextStyle = TextStyle.Default, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = Int.MAX_VALUE, + placeholder: String? = null, + isError: Boolean = false, + showValidationHint: Boolean = true, + showValidationError: Boolean = false, + validationResult: ValidationResult? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = MaterialTheme.shapes.small, + colors: TextFieldColors = TextFieldDefaults.colors(), +) { + val textColor = textStyle.color.takeOrElse { + if (enabled) { + colors.focusedTextColor + } else { + colors.disabledTextColor + } + } + val imeOptions: Int = when (keyboardOptions.imeAction) { + ImeAction.Done -> EditorInfo.IME_ACTION_DONE + ImeAction.Go -> EditorInfo.IME_ACTION_GO + ImeAction.Next -> EditorInfo.IME_ACTION_NEXT + ImeAction.Previous -> EditorInfo.IME_ACTION_PREVIOUS + ImeAction.Search -> EditorInfo.IME_ACTION_SEARCH + ImeAction.Send -> EditorInfo.IME_ACTION_SEND + else -> EditorInfo.IME_ACTION_NONE + } + val inputType: Int = when (keyboardOptions.keyboardType) { + KeyboardType.Text -> InputType.TYPE_CLASS_TEXT + KeyboardType.Number -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_NORMAL + KeyboardType.Phone -> InputType.TYPE_CLASS_PHONE + KeyboardType.Uri -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI + KeyboardType.Email -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + KeyboardType.Password -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + KeyboardType.NumberPassword -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + else -> InputType.TYPE_NULL + } + val inputCapitalization: Int = when (keyboardOptions.capitalization) { + KeyboardCapitalization.Characters -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS + KeyboardCapitalization.Words -> InputType.TYPE_TEXT_FLAG_CAP_WORDS + KeyboardCapitalization.Sentences -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + else -> 0 + } + val mergedTextStyle = textStyle.copy(color = textColor, textDirection = TextDirection.Content) + val isFocused by interactionSource.collectIsFocusedAsState() + val isErrorState = isError || (showValidationError && validationResult?.isInvalid() == true) + + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Surface( + modifier = modifier, + color = if (enabled) { colors.focusedContainerColor} else { colors.disabledContainerColor}, + border = if (isErrorState && enabled) { + BorderStroke(ButtonDefaults.outlinedButtonBorder.width, MaterialTheme.colorScheme.error) + } else if (isFocused) { + BorderStroke(ButtonDefaults.outlinedButtonBorder.width, MaterialTheme.colorScheme.primary) + } else { + ButtonDefaults.outlinedButtonBorder + }, + shape = shape, + ) { + Box( + modifier = Modifier + .defaultMinSize( + minWidth = ButtonDefaults.MinWidth, + minHeight = 40.dp, + ) + .padding(ButtonDefaults.ContentPadding), + contentAlignment = Alignment.CenterStart, + ) { + ProvideTextStyle(value = mergedTextStyle) { + val localTextStyle = LocalTextStyle.current + Row { + var localEditTextReference: EditText? by remember { mutableStateOf(null) } + AndroidView( + modifier = Modifier.fillMaxWidth(0.9f), // Occupy the max size in the Compose UI tree + factory = { context -> + object : EditText(context) { + override fun onTextChanged( + text: CharSequence?, + start: Int, + lengthBefore: Int, + lengthAfter: Int + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + if (text != null) { + onValueChange(text.toString()) + } + } + + public override fun onFocusChanged( + focused: Boolean, + direction: Int, + previouslyFocusedRect: Rect? + ) { + super.onFocusChanged(focused, direction, previouslyFocusedRect) + if (focused) + FlorisImeService.setCurrentInputConnection( + onCreateInputConnection( + EditorInfo() + ) + ) + else + FlorisImeService.setCurrentInputConnection(null) + } + + override fun onEditorAction(actionCode: Int) { + super.onEditorAction(actionCode) + clearFocus() + } + }.apply { + this.isEnabled = enabled + this.isSingleLine = singleLine + this.maxLines = maxLines + this.setTextColor(localTextStyle.color.toArgb()) + localEditTextReference = this + + this.imeOptions = imeOptions + this.inputType = + inputType or if (keyboardOptions.autoCorrect) InputType.TYPE_TEXT_FLAG_AUTO_CORRECT else 0 or inputCapitalization + } + }, + update = {} + ) + IconButton(onClick = { + localEditTextReference?.clearFocus() + }) { + Icon(Icons.Default.Check, null) + } + } + } + if (!placeholder.isNullOrBlank()) { + Text( + text = placeholder, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.56f), + ) + } + } + } + } + + /*if (showValidationHint && validationResult?.isValid() == true && validationResult.hasHintMessage()) { + Text( + text = validationResult.hintMessage(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.56f), + ) + } + + if (showValidationError && validationResult?.isInvalid() == true && validationResult.hasErrorMessage()) { + Text( + text = validationResult.errorMessage(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + }*/ +}