diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/markdown/MarkdownEditor.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/markdown/MarkdownEditor.kt index 5598b9e3..5c19b8f7 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/markdown/MarkdownEditor.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/markdown/MarkdownEditor.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor @@ -32,8 +34,8 @@ import org.intellij.markdown.parser.MarkdownParser @Composable fun MarkdownEditor( - value: String, - onValueChange: (String) -> Unit, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, placeholder: (@Composable () -> Unit)? = null ) { @@ -58,7 +60,46 @@ fun MarkdownEditor( BasicTextField( value = value, - onValueChange = onValueChange, + onValueChange = { + val cursorPosition = if (it.selection.collapsed) it.selection.start else null + // If: + // - multiple chars are selected + // - the last action was not an insert + // - the cursor char before the selection is not a newline + // do nothing. + if (cursorPosition == null || it.text.length <= value.text.length || it.text.getOrNull( + cursorPosition - 1 + ) != '\n' + ) { + onValueChange(it) + } else { + // else check if the previous line was a list, if yes, add a list item + val prevLine = it.text.substring(0, cursorPosition - 1).substringAfterLast('\n') + val leadingSpaces = prevLine.takeWhile { it == ' ' } + val prevLineWithoutLeadingSpaces = prevLine.trimStart() + val listMarker = leadingSpaces + when { + prevLineWithoutLeadingSpaces.startsWith("- [ ] ") -> "- [ ] " + prevLineWithoutLeadingSpaces.startsWith("- [x] ") -> "- [ ] " + prevLineWithoutLeadingSpaces.startsWith("- ") -> "- " + prevLineWithoutLeadingSpaces.startsWith("* ") -> "* " + prevLineWithoutLeadingSpaces.startsWith("+ ") -> "+ " + prevLineWithoutLeadingSpaces.startsWith("1. ") -> "1. " + else -> { + onValueChange(it) + return@BasicTextField + } + } + onValueChange( + it.copy( + text = it.text.substring( + 0, + cursorPosition + ) + listMarker + it.text.substring(cursorPosition), + selection = TextRange(cursorPosition + listMarker.length) + ) + ) + } + }, modifier = modifier.focusRequester(focusRequester), textStyle = MaterialTheme.typography.bodyMedium.copy( color = LocalContentColor.current, @@ -72,7 +113,7 @@ fun MarkdownEditor( ) } else { - if (placeholder != null && value.isBlank()) { + if (placeholder != null && value.text.isBlank()) { Box( modifier = modifier.clickable( indication = null, @@ -91,14 +132,14 @@ fun MarkdownEditor( } } else { MarkdownText( - value, + value.text, modifier = modifier.clickable( indication = null, interactionSource = remember { MutableInteractionSource() }, ) { focused = true }, - onTextChange = onValueChange, + onTextChange = { onValueChange(TextFieldValue(it)) }, ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt index 74db58f0..1a252eed 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.compose.viewModel @@ -93,7 +94,7 @@ fun NotesWidget( } ) - AnimatedVisibility(isLastWidget == false && text.isBlank()) { + AnimatedVisibility(isLastWidget == false && text.text.isBlank()) { IconButton( onClick = { viewModel.dismissNote() @@ -105,7 +106,7 @@ fun NotesWidget( } } - AnimatedVisibility(text.isNotBlank()) { + AnimatedVisibility(text.text.isNotBlank()) { var showMenu by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -168,7 +169,7 @@ fun NotesWidget( } } else { val content = text - viewModel.setText("") + viewModel.setText(TextFieldValue("")) lifecycleOwner.lifecycleScope.launch { val result = snackbarHostState.showSnackbar( message = context.getString(R.string.notes_widget_dismissed), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt index 4872951a..a9d78880 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import android.util.Log import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer @@ -26,7 +27,7 @@ class NotesWidgetVM( ) : ViewModel() { private val widget = MutableStateFlow(null) - val noteText = mutableStateOf(widget.value?.config?.storedText ?: "") + val noteText = mutableStateOf(TextFieldValue(widget.value?.config?.storedText ?: "")) val isLastNoteWidget = widgetsService.countWidgets(NotesWidget.Type).map { it == 1 @@ -35,23 +36,23 @@ class NotesWidgetVM( fun updateWidget(widget: NotesWidget) { val oldId = this.widget.value?.id this.widget.value = widget - if (widget.id != oldId) noteText.value = widget.config.storedText + if (widget.id != oldId) noteText.value = TextFieldValue(widget.config.storedText) } private var updateJob: Job? = null - fun setText(text: String) { + fun setText(text: TextFieldValue) { noteText.value = text updateJob?.cancel() val widget = widget.value ?: return updateJob = viewModelScope.launch { delay(1000) - widgetsService.updateWidget(widget.copy(config = widget.config.copy(storedText = text))) + widgetsService.updateWidget(widget.copy(config = widget.config.copy(storedText = text.text))) } } fun exportNote(context: Context, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { - val text = noteText.value + val text = noteText.value.text Log.d("MM20", text) val outputStream = context.contentResolver.openOutputStream(uri) outputStream?.use {