From 7d0d5f572094e908997f8aa2e74471f72e5e329c Mon Sep 17 00:00:00 2001 From: Yahor Urbanovich Date: Wed, 29 Jan 2025 11:25:08 +0300 Subject: [PATCH] Make clipboard icon name required --- .../ui/foundation/FocusableTextField.kt | 128 ++++++++++++------ .../valkyrie/ui/foundation/icons/Checked.kt | 2 +- .../conversion/IconPackConversionState.kt | 2 +- .../conversion/IconPackConversionViewModel.kt | 28 +--- .../ui/batch/BatchProcessingStateUi.kt | 15 +- .../conversion/ui/batch/ui/IconNameField.kt | 117 ---------------- .../ui/preview/action/ui/EditAction.kt | 7 +- 7 files changed, 111 insertions(+), 188 deletions(-) delete mode 100644 idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/ui/IconNameField.kt diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/FocusableTextField.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/FocusableTextField.kt index 4f50b9ae..d53aaa0b 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/FocusableTextField.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/FocusableTextField.kt @@ -2,19 +2,25 @@ package io.github.composegears.valkyrie.ui.foundation import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -48,6 +54,7 @@ import io.github.composegears.valkyrie.ui.foundation.icons.Checked import io.github.composegears.valkyrie.ui.foundation.icons.Close import io.github.composegears.valkyrie.ui.foundation.icons.Edit import io.github.composegears.valkyrie.ui.foundation.icons.ValkyrieIcons +import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -81,7 +88,15 @@ fun FocusableTextField( .onPointerEvent(PointerEventType.Exit) { isHover = false } .border( width = Dp.Hairline, - color = if (mode == Edit) colors.focusedBorderColor else colors.unfocusedBorderColor, + color = when (mode) { + Edit -> { + when { + isError -> colors.errorColor + else -> colors.focusedBorderColor + } + } + else -> colors.unfocusedBorderColor + }, shape = shape, ) .clip(shape) @@ -95,7 +110,7 @@ fun FocusableTextField( .focusProperties { canFocus = true } .focusRequester(focusRequester) .focusable() - .widthIn(max = 150.dp) + .weight(1f) .height(32.dp) .onKeyEvent { when (it.key) { @@ -139,11 +154,21 @@ fun FocusableTextField( contentAlignment = Alignment.CenterStart, ) { innerTextField() + if (isError && mode == View) { + Text( + text = "icon name required", + style = MaterialTheme.typography.bodyMedium, + color = colors.errorColor, + ) + } } }, ) - AnimatedContent(targetState = mode) { targetMode -> + AnimatedContent( + modifier = Modifier.height(32.dp), + targetState = mode, + ) { targetMode -> when (targetMode) { View -> { Icon( @@ -161,38 +186,48 @@ fun FocusableTextField( ) } Edit -> { - Row { - Icon( - imageVector = ValkyrieIcons.Close, - contentDescription = null, - modifier = Modifier - .padding(4.dp) - .clip(shape) - .clickable { - focusRequester.freeFocus() - mode = View - textFieldValue = textFieldValue.copy(text = value) - } - .padding(4.dp), - ) - Icon( - imageVector = ValkyrieIcons.Checked, - contentDescription = null, - modifier = Modifier - .padding(4.dp) - .clip(shape) - .clickable(enabled = !isError) { - focusRequester.freeFocus() - mode = View - onValueChange(textFieldValue.text) - } - .padding(4.dp), - tint = if (isError) { - LocalContentColor.current.disabled() - } else { - LocalContentColor.current + CenterVerticalRow( + modifier = Modifier.padding(end = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FilledIconButton( + modifier = Modifier.size(20.dp), + colors = IconButtonDefaults.filledIconButtonColors() + .copy(containerColor = MaterialTheme.colorScheme.surfaceVariant), + onClick = { + focusRequester.freeFocus() + mode = View + textFieldValue = textFieldValue.copy(text = value) }, - ) + ) { + Icon( + modifier = Modifier.size(12.dp), + imageVector = ValkyrieIcons.Close, + contentDescription = null, + ) + } + FilledIconButton( + modifier = Modifier.size(20.dp), + colors = IconButtonDefaults.filledIconButtonColors() + .copy(containerColor = MaterialTheme.colorScheme.primary.disabled()), + enabled = !isError, + onClick = { + focusRequester.freeFocus() + mode = View + onValueChange(textFieldValue.text) + }, + ) { + Icon( + modifier = Modifier.size(12.dp), + imageVector = ValkyrieIcons.Checked, + contentDescription = null, + tint = if (isError) { + LocalContentColor.current.disabled() + } else { + LocalContentColor.current + }, + ) + } } } } @@ -212,9 +247,9 @@ object FocusableTextFieldDefaults { return FocusableTextFieldColor( focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant.dim(), + unfocusedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), + focusedBorderColor = MaterialTheme.colorScheme.primary, cursorColor = MaterialTheme.colorScheme.onSurfaceVariant, - focusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), - unfocusedBorderColor = Color.Transparent, errorColor = MaterialTheme.colorScheme.error, ) } @@ -233,3 +268,20 @@ private enum class Mode { Edit, View, } + +@Preview +@Composable +private fun FocusableTextFieldPreview() = PreviewTheme { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + FocusableTextField( + modifier = Modifier.width(300.dp), + value = "IconName", + onValueChange = {}, + ) + FocusableTextField( + modifier = Modifier.width(150.dp), + value = "IconName", + onValueChange = {}, + ) + } +} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/icons/Checked.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/icons/Checked.kt index 2e4c4a46..0c1db452 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/icons/Checked.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/icons/Checked.kt @@ -22,7 +22,7 @@ val ValkyrieIcons.Checked: ImageVector ).apply { path( stroke = SolidColor(Color(0xFF6C707E)), - strokeLineWidth = 1.5f, + strokeLineWidth = 1f, strokeLineCap = StrokeCap.Round, strokeLineJoin = StrokeJoin.Round, ) { diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt index 92f490ef..d63d447f 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt @@ -39,7 +39,7 @@ sealed interface BatchIcon { } @JvmInline -value class IconName(val value: String) +value class IconName(val name: String) @JvmInline value class IconId(val id: String) diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt index d4eb2c9e..bffd57ae 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt @@ -43,8 +43,6 @@ class IconPackConversionViewModel( private val _events = MutableSharedFlow() val events = _events.asSharedFlow() - private var clipboardIconCounter = 0 - init { val restoredState = savedState?.getOrNull>(key = "icons") @@ -69,16 +67,11 @@ class IconPackConversionViewModel( } } } - - savedState?.getOrNull(key = "clipboardIconCounter")?.also { clipboardIconCounter = it } } override fun saveToSaveState(): SavedState { return when (val state = _state.value) { - is BatchProcessing.IconPackCreationState -> mapOf( - "icons" to state.icons, - "clipboardIconCounter" to clipboardIconCounter, - ) + is BatchProcessing.IconPackCreationState -> mapOf("icons" to state.icons) else -> mapOf("icons" to emptyList>()) } } @@ -98,7 +91,6 @@ class IconPackConversionViewModel( val iconsToProcess = icons.filter { it.id != iconId } if (iconsToProcess.isEmpty()) { - clipboardIconCounter = 0 IconsPickering } else { copy( @@ -140,7 +132,7 @@ class IconPackConversionViewModel( val settings = inMemorySettings.current val output = ImageVectorGenerator.convert( vector = icon.irImageVector, - iconName = icon.iconName.value, + iconName = icon.iconName.name, config = ImageVectorGeneratorConfig( packageName = icon.iconPack.iconPackage, iconPackPackage = settings.iconPackPackage, @@ -175,7 +167,7 @@ class IconPackConversionViewModel( is IconPack.Nested -> { val vectorSpecOutput = ImageVectorGenerator.convert( vector = icon.irImageVector, - iconName = icon.iconName.value, + iconName = icon.iconName.name, config = ImageVectorGeneratorConfig( packageName = icon.iconPack.iconPackage, iconPackPackage = settings.iconPackPackage, @@ -201,7 +193,7 @@ class IconPackConversionViewModel( is IconPack.Single -> { val vectorSpecOutput = ImageVectorGenerator.convert( vector = icon.irImageVector, - iconName = icon.iconName.value, + iconName = icon.iconName.name, config = ImageVectorGeneratorConfig( packageName = icon.iconPack.iconPackage, iconPackPackage = settings.iconPackPackage, @@ -254,16 +246,10 @@ class IconPackConversionViewModel( fun reset() { _state.updateState { IconsPickering } - clipboardIconCounter = 0 } private fun processText(text: String) = viewModelScope.launch(Dispatchers.Default) { - val iconName = when (clipboardIconCounter) { - 0 -> "IconName" - else -> "IconName_$clipboardIconCounter" - } - clipboardIconCounter++ - + val iconName = "" val output = runCatching { SvgXmlParser.toIrImageVector(text, iconName) }.getOrNull() val icon = when (output) { @@ -334,10 +320,10 @@ class IconPackConversionViewModel( private fun List.isAllIconsValid() = isNotEmpty() && all { it is BatchIcon.Valid } && - all { it.iconName.value.isNotEmpty() && !it.iconName.value.contains(" ") } && + all { it.iconName.name.isNotEmpty() && !it.iconName.name.contains(" ") } && hasNoDuplicates() - private fun List.hasNoDuplicates() = map { it.iconName.value }.toSet().size == size + private fun List.hasNoDuplicates() = map { it.iconName.name }.toSet().size == size } sealed interface ConversionEvent { diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/BatchProcessingStateUi.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/BatchProcessingStateUi.kt index ad830c17..4f1dcf3b 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/BatchProcessingStateUi.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/BatchProcessingStateUi.kt @@ -45,6 +45,7 @@ import io.github.composegears.valkyrie.ui.domain.model.PreviewType import io.github.composegears.valkyrie.ui.foundation.AppBarTitle import io.github.composegears.valkyrie.ui.foundation.CenterVerticalRow import io.github.composegears.valkyrie.ui.foundation.CloseAction +import io.github.composegears.valkyrie.ui.foundation.FocusableTextField import io.github.composegears.valkyrie.ui.foundation.IconButton import io.github.composegears.valkyrie.ui.foundation.SettingsAction import io.github.composegears.valkyrie.ui.foundation.TopAppBar @@ -64,7 +65,6 @@ import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.Picker import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.PickerEvent.PickFiles import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.ClipboardEventColumn import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.ui.FileTypeBadge -import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.ui.IconNameField import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.ui.IconPreviewBox @Composable @@ -166,11 +166,13 @@ private fun ValidIconItem( irImageVector = icon.irImageVector, previewType = previewType, ) - IconNameField( + + val name = icon.iconName.name + FocusableTextField( modifier = Modifier .weight(1f) .padding(end = 32.dp), - value = icon.iconName.value, + value = name, onValueChange = { onRenameIcon(icon, IconName(it)) }, @@ -248,7 +250,10 @@ private fun BrokenIconItem( modifier = Modifier .weight(1f) .padding(vertical = 8.dp), - text = "Failed to parse icon: ${broken.iconName.value}", + text = when { + broken.iconName.name.isEmpty() -> "Failed to parse icon" + else -> "Failed to parse icon: ${broken.iconName.name}" + }, ) IconButton( imageVector = Icons.Default.Delete, @@ -364,7 +369,7 @@ private fun BatchProcessingStatePreview() = PreviewTheme { ), BatchIcon.Broken( id = IconId("2"), - iconName = IconName(value = "ic_all_path_params_3"), + iconName = IconName(name = "ic_all_path_params_3"), ), BatchIcon.Valid( id = IconId("3"), diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/ui/IconNameField.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/ui/IconNameField.kt deleted file mode 100644 index 12e944ef..00000000 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/ui/IconNameField.kt +++ /dev/null @@ -1,117 +0,0 @@ -package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.ui - -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.indication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.unit.dp -import io.github.composegears.valkyrie.ui.foundation.BaseTextField -import io.github.composegears.valkyrie.ui.foundation.IconButton -import io.github.composegears.valkyrie.ui.foundation.rememberMutableState -import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme - -@Composable -fun IconNameField( - value: String, - modifier: Modifier = Modifier, - onValueChange: (String) -> Unit, -) { - val focusManager = LocalFocusManager.current - - var text by rememberMutableState(value) { value } - var isFocused by rememberMutableState { false } - - val interactionSource = remember { MutableInteractionSource() } - - BaseTextField( - modifier = modifier - .fillMaxWidth() - .height(40.dp) - .clip(RoundedCornerShape(8.dp)) - .indication(interactionSource, LocalIndication.current) - .hoverable(interactionSource) - .onFocusChanged { - isFocused = it.isFocused - if (!isFocused) { - onValueChange(text) - } - } - .onKeyEvent { - when (it.key) { - Key.Escape -> { - focusManager.clearFocus() - text = value - true - } - Key.Enter -> { - onValueChange(text) - focusManager.clearFocus() - true - } - else -> false - } - }, - value = text, - onValueChange = { text = it }, - isError = text.isEmpty() || text.contains(" "), - placeholder = if (text.isEmpty()) { - { - Text( - text = "Could not be empty", - color = MaterialTheme.colorScheme.onError, - ) - } - } else { - null - }, - trailingIcon = if (isFocused) { - { - IconButton( - modifier = Modifier - .size(32.dp) - .clip(CircleShape), - imageVector = Icons.Default.Check, - iconSize = 18.dp, - onClick = { - onValueChange(text) - focusManager.clearFocus() - }, - ) - } - } else { - null - }, - ) -} - -@Preview -@Composable -private fun IconNameFieldPreview() = PreviewTheme { - IconNameField( - modifier = Modifier.width(300.dp), - value = "IconName", - onValueChange = {}, - ) -} diff --git a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/simple/conversion/ui/preview/action/ui/EditAction.kt b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/simple/conversion/ui/preview/action/ui/EditAction.kt index b44d2da8..cf09bb45 100644 --- a/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/simple/conversion/ui/preview/action/ui/EditAction.kt +++ b/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/simple/conversion/ui/preview/action/ui/EditAction.kt @@ -4,6 +4,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -11,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.github.composegears.valkyrie.ui.foundation.FocusableTextField -import io.github.composegears.valkyrie.ui.foundation.FocusableTextFieldDefaults import io.github.composegears.valkyrie.ui.foundation.HorizontalDivider import io.github.composegears.valkyrie.ui.foundation.disabled import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme @@ -32,12 +32,9 @@ fun EditAction( color = LocalContentColor.current.disabled(), ) FocusableTextField( + modifier = Modifier.width(200.dp), value = iconName, onValueChange = onNameChange, - colors = FocusableTextFieldDefaults.colors().copy( - unfocusedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), - focusedBorderColor = MaterialTheme.colorScheme.onSurface, - ), ) } }