diff --git a/TODO.md b/TODO.md index 0dd7f4fc..2640e0fc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ - Multi edit icon pack - Generated code as val or lazy delegate - Rename icon in icon pack -- Preview icon on square background pattern - Simple mode without package - Add version into plugin settings - Export/import iconpack configuration into file @@ -9,3 +8,5 @@ - Generate preview for whole icon pack - Add limitations for ImageVector - Update Readme demo with latest plugin design +- Continues drag & drop +- Add changelog into plugin \ No newline at end of file diff --git a/components/parser/build.gradle.kts b/components/parser/build.gradle.kts index 755be242..32cb097e 100644 --- a/components/parser/build.gradle.kts +++ b/components/parser/build.gradle.kts @@ -12,4 +12,6 @@ dependencies { implementation(libs.android.build.tools) implementation(libs.kotlin.io) + + testImplementation(libs.kotlin.test) } \ No newline at end of file diff --git a/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/IconParser.kt b/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/IconParser.kt index 21aebf6e..5aca4a0d 100644 --- a/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/IconParser.kt +++ b/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/IconParser.kt @@ -21,7 +21,7 @@ object IconParser { fun toVector(file: File): IconParserOutput { val iconType = IconTypeParser.getIconType(file.extension) ?: error("File not SVG or XML") - val fileName = getFileName(fileName = file.name, iconType = iconType) + val fileName = getIconName(fileName = file.name) val icon = when (iconType) { SVG -> { val tmpFile = createTempFile(suffix = "valkyrie/") @@ -38,21 +38,14 @@ object IconParser { ) } - private fun getFileName(fileName: String, iconType: IconType): String { - - var name = fileName - .removeSuffix(".${iconType.extension}") - .split("_") - .joinToString("") { it.capitalized() } - .replace("\\d".toRegex(), "") - - if (name.startsWith("ic", ignoreCase = true)) { - name = name.drop(2).capitalized() - } - return name - } -} - -private fun String.capitalized(): String = replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + fun getIconName(fileName: String) = fileName + .removePrefix("-") + .removePrefix("_") + .removeSuffix(".svg") + .removeSuffix(".xml") + .removePrefix("ic_") + .removePrefix("ic-") + .replace("[^a-zA-Z0-9\\-_ ]".toRegex(), "_") + .split("_", "-") + .joinToString(separator = "") { it.lowercase().capitalized() } } \ No newline at end of file diff --git a/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/String.kt b/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/String.kt new file mode 100644 index 00000000..a7e2cdff --- /dev/null +++ b/components/parser/src/main/kotlin/io/github/composegears/valkyrie/parser/String.kt @@ -0,0 +1,14 @@ +package io.github.composegears.valkyrie.parser + +import java.util.* + +fun String.removePrefix(prefix: CharSequence): String { + if (startsWith(prefix, ignoreCase = true)) { + return substring(prefix.length) + } + return this +} + +fun String.capitalized(): String = replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() +} \ No newline at end of file diff --git a/components/parser/src/test/kotlin/io/github/composegears/valkyrie/parser/IconParserTest.kt b/components/parser/src/test/kotlin/io/github/composegears/valkyrie/parser/IconParserTest.kt new file mode 100644 index 00000000..e74459d3 --- /dev/null +++ b/components/parser/src/test/kotlin/io/github/composegears/valkyrie/parser/IconParserTest.kt @@ -0,0 +1,44 @@ +package io.github.composegears.valkyrie.parser + +import kotlin.test.Test +import kotlin.test.assertEquals + +class IconParserTest { + + private data class IconTest( + val fileName: String, + val expected: String + ) + + @Test + fun `test icon name`() { + val fileNames = listOf( + IconTest(fileName = "ic_test_icon.svg", expected = "TestIcon"), + IconTest(fileName = "ic_test_icon.xml", expected = "TestIcon"), + IconTest(fileName = "test_icon.svg", expected = "TestIcon"), + IconTest(fileName = "ic_test_icon2.svg", expected = "TestIcon2"), + IconTest(fileName = "ic_test_icon_name.svg", expected = "TestIconName"), + IconTest(fileName = "ic_testicon.svg", expected = "Testicon"), + IconTest(fileName = "ic_test_icon_name_with_underscores.svg", expected = "TestIconNameWithUnderscores"), + IconTest(fileName = "ic_TESTIcon.svg", expected = "Testicon"), + IconTest(fileName = "ic-test-icon.svg", expected = "TestIcon"), + IconTest(fileName = "ic_test@icon!.svg", expected = "TestIcon"), + IconTest(fileName = "ic_test_icon123.xml", expected = "TestIcon123"), + IconTest(fileName = "my_icon.xml", expected = "MyIcon"), + IconTest(fileName = "Ic_TeSt123Icon.svg", expected = "Test123icon"), + IconTest(fileName = "ic_special@#\$%^&*()icon.svg", expected = "SpecialIcon"), + IconTest(fileName = "ic--test__icon---name.svg", expected = "TestIconName"), + IconTest(fileName = "@#$%.svg", expected = ""), + IconTest(fileName = "", expected = ""), + IconTest(fileName = "-_ic_test_icon_-.svg", expected = "TestIcon"), + IconTest(fileName = "pos_1", expected = "Pos1"), + IconTest(fileName = "1", expected = "1"), + ) + + fileNames.forEach { + val iconName = IconParser.getIconName(it.fileName) + + assertEquals(expected = it.expected, actual = iconName) + } + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/BaseTextField.kt b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/BaseTextField.kt new file mode 100644 index 00000000..f04d7c53 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/BaseTextField.kt @@ -0,0 +1,170 @@ +package io.github.composegears.valkyrie.ui.foundation + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import io.github.composegears.valkyrie.ui.foundation.theme.PreviewTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BaseTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current.copy( + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ), + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = true, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = RoundedCornerShape(8.dp), + colors: TextFieldColors = TextFieldDefaults.colors().copy( + focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + cursorColor = MaterialTheme.colorScheme.onSurfaceVariant, + errorTextColor = MaterialTheme.colorScheme.onError, + errorContainerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.5f), + errorCursorColor = MaterialTheme.colorScheme.onError, + errorTrailingIconColor = MaterialTheme.colorScheme.onError, + errorIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), +) { + val textColor = textStyle.color.takeOrElse { + colors.textColor(enabled, isError, interactionSource).value + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { + BasicTextField( + value = value, + modifier = modifier + .defaultMinSize( + minWidth = TextFieldDefaults.MinWidth, + minHeight = TextFieldDefaults.MinHeight + ), + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + textStyle = mergedTextStyle, + cursorBrush = SolidColor(colors.cursorColor(isError).value), + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.DecorationBox( + contentPadding = PaddingValues(horizontal = 8.dp), + value = value, + visualTransformation = visualTransformation, + innerTextField = innerTextField, + placeholder = placeholder, + label = label, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + shape = shape, + singleLine = singleLine, + enabled = enabled, + isError = isError, + interactionSource = interactionSource, + colors = colors, + ) + } + ) + } +} + +@Composable +private fun TextFieldColors.cursorColor(isError: Boolean): State { + return rememberUpdatedState(if (isError) errorCursorColor else cursorColor) +} + +@Composable +private fun TextFieldColors.textColor( + enabled: Boolean, + isError: Boolean, + interactionSource: InteractionSource +): State { + val focused by interactionSource.collectIsFocusedAsState() + + val targetValue = when { + !enabled -> disabledTextColor + isError -> errorTextColor + focused -> focusedTextColor + else -> unfocusedTextColor + } + return rememberUpdatedState(targetValue) +} + +@Preview +@Composable +private fun BaseTextFieldPreview() = PreviewTheme { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BaseTextField( + value = "Hello, World!", + onValueChange = {}, + isError = false + ) + BaseTextField( + value = "Hello, World!", + onValueChange = {}, + isError = true + ) + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionScreen.kt b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionScreen.kt index 674bc31a..3824274c 100644 --- a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionScreen.kt +++ b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionScreen.kt @@ -3,6 +3,7 @@ package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.defaultMinSize @@ -27,6 +28,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import com.composegears.tiamat.koin.koinTiamatViewModel import com.composegears.tiamat.navController @@ -34,7 +37,6 @@ import com.composegears.tiamat.navDestination import com.composegears.tiamat.navigationSlideInOut import com.intellij.openapi.application.writeAction import com.intellij.openapi.vfs.VirtualFileManager -import io.github.composegears.valkyrie.settings.ValkyriesSettings import io.github.composegears.valkyrie.ui.foundation.AppBarTitle import io.github.composegears.valkyrie.ui.foundation.ClearAction import io.github.composegears.valkyrie.ui.foundation.SettingsAction @@ -54,7 +56,6 @@ val IconPackConversionScreen by navDestination { val viewModel = koinTiamatViewModel() val state by viewModel.state.collectAsState() - val settings by viewModel.valkyriesSettings.collectAsState() LaunchedEffect(Unit) { viewModel.events @@ -77,7 +78,6 @@ val IconPackConversionScreen by navDestination { IconPackConversionUi( state = state, - settings = settings, openSettings = { navController.navigate( dest = SettingsScreen, @@ -89,21 +89,22 @@ val IconPackConversionScreen by navDestination { onDeleteIcon = viewModel::deleteIcon, onReset = viewModel::reset, onPreviewClick = viewModel::showPreview, - onExport = viewModel::export + onExport = viewModel::export, + onRenameIcon = viewModel::renameIcon ) } @Composable private fun IconPackConversionUi( state: IconPackConversionState, - settings: ValkyriesSettings, openSettings: () -> Unit, onPickEvent: (PickerEvent) -> Unit, updatePack: (BatchIcon, String) -> Unit, onDeleteIcon: (IconName) -> Unit, onReset: () -> Unit, onPreviewClick: (IconName) -> Unit, - onExport: () -> Unit + onExport: () -> Unit, + onRenameIcon: (BatchIcon, IconName) -> Unit ) { var isVisible by rememberSaveable { mutableStateOf(true) } @@ -122,7 +123,13 @@ private fun IconPackConversionUi( } } - Box { + val focusManager = LocalFocusManager.current + + Box(modifier = Modifier + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + } + ) { Column(modifier = Modifier.fillMaxSize()) { TopAppBar { if (state is BatchFilesProcessing) { @@ -142,7 +149,8 @@ private fun IconPackConversionUi( icons = state.iconsToProcess, onDeleteIcon = onDeleteIcon, onUpdatePack = updatePack, - onPreviewClick = onPreviewClick + onPreviewClick = onPreviewClick, + onRenameIcon = onRenameIcon ) } } diff --git a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt index 53a40e04..b2ece20c 100644 --- a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt +++ b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionState.kt @@ -12,7 +12,12 @@ sealed interface IconPackConversionState { ) : IconPackConversionState { val exportEnabled: Boolean - get() = iconsToProcess.isNotEmpty() && iconsToProcess.all { it is BatchIcon.Valid } + get() = iconsToProcess.isNotEmpty() && + iconsToProcess.all { it is BatchIcon.Valid } && + iconsToProcess.all { icon -> + icon.iconName.value.isNotEmpty() && + !icon.iconName.value.contains(" ") + } } } diff --git a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt index 818e9392..6eb1e26a 100644 --- a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt +++ b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/IconPackConversionViewModel.kt @@ -30,7 +30,7 @@ class IconPackConversionViewModel( private val _events = MutableSharedFlow() val events = _events.asSharedFlow() - val valkyriesSettings = inMemorySettings.settings + private val valkyriesSettings = inMemorySettings.settings fun pickerEvent(events: PickerEvent) { when (events) { @@ -89,7 +89,7 @@ class IconPackConversionViewModel( ImageVectorGenerator.convert( vector = parserOutput.vector, - kotlinName = parserOutput.kotlinName, + kotlinName = iconName.value, config = ImageVectorGeneratorConfig( packageName = icon.iconPack.iconPackage, packName = valkyriesSettings.value.iconPackName, @@ -116,7 +116,7 @@ class IconPackConversionViewModel( val parserOutput = IconParser.toVector(icon.file) val vectorSpecOutput = ImageVectorGenerator.convert( vector = parserOutput.vector, - kotlinName = parserOutput.kotlinName, + kotlinName = icon.iconName.value, config = ImageVectorGeneratorConfig( packageName = icon.iconPack.iconPackage, packName = valkyriesSettings.value.iconPackName, @@ -160,6 +160,28 @@ class IconPackConversionViewModel( } } + fun renameIcon(batchIcon: BatchIcon, newName: IconName) { + _state.updateState { + when (this) { + is IconsPickering -> this + is BatchFilesProcessing -> { + copy( + iconsToProcess = iconsToProcess.map { icon -> + if (icon.iconName == batchIcon.iconName) { + when (icon) { + is BatchIcon.Broken -> icon + is BatchIcon.Valid -> icon.copy(iconName = newName) + } + } else { + icon + } + } + ) + } + } + } + } + fun reset() { _state.updateState { IconsPickering } } @@ -172,17 +194,17 @@ class IconPackConversionViewModel( BatchFilesProcessing( iconsToProcess = files .sortedBy { it.name } - .map { - when (val painter = it.toPainterOrNull()) { + .map { file -> + when (val painter = file.toPainterOrNull()) { null -> BatchIcon.Broken( - iconName = IconName(it.nameWithoutExtension), - extension = it.extension + iconName = IconName(file.nameWithoutExtension), + extension = file.extension ) else -> BatchIcon.Valid( - iconName = IconName(it.nameWithoutExtension), - extension = it.extension, + iconName = IconName(IconParser.getIconName(file.name)), + extension = file.extension, iconPack = inMemorySettings.current.buildDefaultIconPack(), - file = it, + file = file, painter = painter ) } @@ -221,7 +243,7 @@ class IconPackConversionViewModel( sealed interface ConversionEvent { data class OpenPreview(val iconContent: String) : ConversionEvent - data object ExportCompleted: ConversionEvent + data object ExportCompleted : ConversionEvent } sealed interface PickerEvent { diff --git a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/BatchProcessingState.kt b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/BatchProcessingState.kt index 43f50532..163b8ea3 100644 --- a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/BatchProcessingState.kt +++ b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/BatchProcessingState.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import io.github.composegears.valkyrie.parser.IconParser import io.github.composegears.valkyrie.ui.foundation.IconButton import io.github.composegears.valkyrie.ui.foundation.icons.ValkyrieIcons import io.github.composegears.valkyrie.ui.foundation.icons.Visibility @@ -46,6 +46,7 @@ import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.BatchI import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconName import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.IconPack import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.FileTypeBadge +import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.IconNameField import io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch.IconPreviewBox import java.io.File @@ -57,6 +58,7 @@ fun BatchProcessingState( onDeleteIcon: (IconName) -> Unit, onUpdatePack: (BatchIcon, String) -> Unit, onPreviewClick: (IconName) -> Unit, + onRenameIcon: (BatchIcon, IconName) -> Unit ) { LazyVerticalGrid( modifier = modifier.fillMaxSize(), @@ -77,7 +79,8 @@ fun BatchProcessingState( icon = batchIcon, onUpdatePack = onUpdatePack, onDeleteIcon = onDeleteIcon, - onPreview = onPreviewClick + onPreview = onPreviewClick, + onRenameIcon = onRenameIcon ) } } @@ -90,7 +93,8 @@ private fun ValidIconItem( icon: BatchIcon.Valid, onUpdatePack: (BatchIcon, String) -> Unit, onPreview: (IconName) -> Unit, - onDeleteIcon: (IconName) -> Unit + onDeleteIcon: (IconName) -> Unit, + onRenameIcon: (BatchIcon, IconName) -> Unit ) { Card(modifier = modifier.fillMaxWidth()) { Box { @@ -106,16 +110,17 @@ private fun ValidIconItem( .fillMaxWidth() .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { IconPreviewBox(painter = icon.painter) - Text( + IconNameField( modifier = Modifier .weight(1f) .padding(end = 32.dp), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - text = icon.iconName.value + value = icon.iconName.value, + onValueChange = { + onRenameIcon(icon, IconName(it)) + } ) } when (icon.iconPack) { @@ -301,7 +306,7 @@ private fun BatchProcessingStatePreview() = PreviewTheme { BatchProcessingState( icons = listOf( BatchIcon.Valid( - iconName = IconName("ic_all_path_params_1"), + iconName = IconName(IconParser.getIconName("ic_all_path_params_1")), extension = "xml", file = File(""), iconPack = IconPack.Single( @@ -310,8 +315,12 @@ private fun BatchProcessingStatePreview() = PreviewTheme { ), painter = painterResource("META-INF/pluginIcon.svg"), ), + BatchIcon.Broken( + iconName = IconName("ic_all_path_params_3"), + extension = "svg" + ), BatchIcon.Valid( - iconName = IconName("ic_all_path_params_2"), + iconName = IconName(IconParser.getIconName("ic_all_path")), extension = "svg", file = File(""), iconPack = IconPack.Nested( @@ -322,13 +331,10 @@ private fun BatchProcessingStatePreview() = PreviewTheme { ), painter = painterResource("META-INF/pluginIcon.svg"), ), - BatchIcon.Broken( - iconName = IconName("ic_all_path_params_3"), - extension = "svg" - ), ), onDeleteIcon = {}, onUpdatePack = { _, _ -> }, - onPreviewClick = {} + onPreviewClick = {}, + onRenameIcon = { _, _ -> } ) } \ No newline at end of file diff --git a/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/IconNameField.kt b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/IconNameField.kt new file mode 100644 index 00000000..c6c025aa --- /dev/null +++ b/plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/conversion/ui/batch/IconNameField.kt @@ -0,0 +1,123 @@ +package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.batch + +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.Box +import androidx.compose.foundation.layout.fillMaxSize +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.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +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 { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + IconNameField( + modifier = Modifier.width(300.dp), + value = "IconName", + onValueChange = {} + ) + } +} \ No newline at end of file