Skip to content

Commit

Permalink
[IconPack] Allow to paste icon from clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
egorikftp committed Nov 11, 2024
1 parent 1ccc4b9 commit 82e3135
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ import com.composegears.tiamat.rememberSaveableViewModel
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.vfs.VirtualFileManager
import io.git.luolix.topposegears.valkyrie.service.GlobalEventsHandler.PendingPathData
import io.git.luolix.topposegears.valkyrie.ui.foundation.AppBarTitle
import io.git.luolix.topposegears.valkyrie.ui.foundation.BackAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.CloseAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.SettingsAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.TopAppBar
import io.git.luolix.topposegears.valkyrie.ui.foundation.WeightSpacer
import io.git.luolix.topposegears.valkyrie.ui.foundation.theme.PreviewTheme
import io.git.luolix.topposegears.valkyrie.ui.screen.mode.iconpack.conversion.IconPackConversionState.BatchProcessing.ExportingState
import io.git.luolix.topposegears.valkyrie.ui.screen.mode.iconpack.conversion.IconPackConversionState.BatchProcessing.IconPackCreationState
Expand Down Expand Up @@ -152,35 +146,13 @@ private fun IconPackConversionUi(

Box(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(onTap = { focusManager.clearFocus() })
.pointerInput(state) {
if (state is IconPackCreationState) {
detectTapGestures(onTap = { focusManager.clearFocus() })
}
},
) {
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar {
AnimatedContent(
targetState = state,
transitionSpec = { fadeIn() togetherWith fadeOut() },
contentKey = {
when (it) {
is IconsPickering, ExportingState, ImportValidationState -> 0
is IconPackCreationState -> 1
}
},
) { current ->
when (current) {
is IconsPickering, ExportingState, ImportValidationState -> {
BackAction(onBack = onBack)
}
is IconPackCreationState -> {
CloseAction(onClose = onReset)
}
}
}
AppBarTitle(title = "IconPack generation")
WeightSpacer()
SettingsAction(openSettings = openSettings)
}
AnimatedContent(
modifier = Modifier.fillMaxSize(),
targetState = state,
Expand All @@ -198,7 +170,11 @@ private fun IconPackConversionUi(
) { current ->
when (current) {
is IconsPickering -> {
IconPackPickerStateUi(onPickerEvent = onPickEvent)
IconPackPickerStateUi(
onPickerEvent = onPickEvent,
onBack = onBack,
openSettings = openSettings,
)
}
ExportingState -> {
LoadingStateUi(message = "Exporting icons...")
Expand All @@ -214,6 +190,8 @@ private fun IconPackConversionUi(
onUpdatePack = updatePack,
onPreviewClick = onPreviewClick,
onRenameIcon = onRenameIcon,
onClose = onReset,
openSettings = openSettings,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class IconPackConversionViewModel(
when (events) {
is PickerEvent.PickDirectory -> events.path.listDirectoryEntries().processFiles()
is PickerEvent.PickFiles -> events.paths.processFiles()
is PickerEvent.ClipboardText -> processText(events.text)
}
}

Expand Down Expand Up @@ -257,6 +258,30 @@ class IconPackConversionViewModel(
_state.updateState { IconsPickering }
}

private fun processText(text: String) = viewModelScope.launch(Dispatchers.Default) {
val iconName = "IconName"

val output = runCatching { SvgXmlParser.toIrImageVector(text, iconName) }.getOrNull()

val icon = when (output) {
null -> BatchIcon.Broken(iconName = IconName(iconName))
else -> BatchIcon.Valid(
iconName = IconName(output.iconName),
iconType = output.iconType,
iconPack = inMemorySettings.current.buildDefaultIconPack(),
irImageVector = output.irImageVector,
)
}
_state.updateState {
val icons = listOf(icon)

BatchProcessing.IconPackCreationState(
icons = icons,
exportEnabled = icons.isAllIconsValid(),
)
}
}

private fun List<Path>.processFiles() = viewModelScope.launch(Dispatchers.Default) {
val paths = filter { it.isRegularFile() && (it.isXml || it.isSvg) }

Expand Down Expand Up @@ -316,4 +341,5 @@ sealed interface ConversionEvent {
sealed interface PickerEvent {
data class PickDirectory(val path: Path) : PickerEvent
data class PickFiles(val paths: List<Path>) : PickerEvent
data class ClipboardText(val text: String) : PickerEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ import io.git.luolix.topposegears.valkyrie.ir.IR_STUB
import io.git.luolix.topposegears.valkyrie.parser.svgxml.IconNameFormatter
import io.git.luolix.topposegears.valkyrie.parser.svgxml.util.IconType.SVG
import io.git.luolix.topposegears.valkyrie.parser.svgxml.util.IconType.XML
import io.git.luolix.topposegears.valkyrie.ui.foundation.AppBarTitle
import io.git.luolix.topposegears.valkyrie.ui.foundation.CenterVerticalRow
import io.git.luolix.topposegears.valkyrie.ui.foundation.CloseAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.IconButton
import io.git.luolix.topposegears.valkyrie.ui.foundation.SettingsAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.TopAppBar
import io.git.luolix.topposegears.valkyrie.ui.foundation.VerticalScrollbar
import io.git.luolix.topposegears.valkyrie.ui.foundation.WeightSpacer
import io.git.luolix.topposegears.valkyrie.ui.foundation.icons.ValkyrieIcons
import io.git.luolix.topposegears.valkyrie.ui.foundation.icons.Visibility
import io.git.luolix.topposegears.valkyrie.ui.foundation.rememberMutableState
Expand All @@ -57,40 +62,50 @@ import io.git.luolix.topposegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.bat
@Composable
fun BatchProcessingStateUi(
state: IconPackCreationState,
onClose: () -> Unit,
openSettings: () -> Unit,
onDeleteIcon: (IconName) -> Unit,
onUpdatePack: (BatchIcon, String) -> Unit,
onPreviewClick: (IconName) -> Unit,
onRenameIcon: (BatchIcon, IconName) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
val lazyGridState = rememberLazyGridState()
Column(modifier = modifier) {
TopAppBar {
CloseAction(onClose = onClose)
AppBarTitle(title = "IconPack generation")
WeightSpacer()
SettingsAction(openSettings = openSettings)
}
Box {
val lazyGridState = rememberLazyGridState()

LazyVerticalGrid(
state = lazyGridState,
modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(items = state.icons, key = { it.id }) { batchIcon ->
when (batchIcon) {
is BatchIcon.Broken -> BrokenIconItem(
broken = batchIcon,
onDelete = onDeleteIcon,
)
is BatchIcon.Valid -> ValidIconItem(
icon = batchIcon,
onUpdatePack = onUpdatePack,
onDeleteIcon = onDeleteIcon,
onPreview = onPreviewClick,
onRenameIcon = onRenameIcon,
)
LazyVerticalGrid(
state = lazyGridState,
modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(items = state.icons, key = { it.id }) { batchIcon ->
when (batchIcon) {
is BatchIcon.Broken -> BrokenIconItem(
broken = batchIcon,
onDelete = onDeleteIcon,
)
is BatchIcon.Valid -> ValidIconItem(
icon = batchIcon,
onUpdatePack = onUpdatePack,
onDeleteIcon = onDeleteIcon,
onPreview = onPreviewClick,
onRenameIcon = onRenameIcon,
)
}
}
}
VerticalScrollbar(adapter = rememberScrollbarAdapter(lazyGridState))
}
VerticalScrollbar(adapter = rememberScrollbarAdapter(lazyGridState))
}
}

Expand Down Expand Up @@ -328,6 +343,8 @@ private fun BatchProcessingStatePreview() = PreviewTheme {
),
),
),
onClose = {},
openSettings = {},
onDeleteIcon = {},
onUpdatePack = { _, _ -> },
onPreviewClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.git.luolix.topposegears.valkyrie.ui.screen.mode.iconpack.conversion.ui.pi
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
Expand All @@ -14,26 +15,47 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.git.luolix.topposegears.valkyrie.ui.foundation.AppBarTitle
import io.git.luolix.topposegears.valkyrie.ui.foundation.BackAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.CenterVerticalRow
import io.git.luolix.topposegears.valkyrie.ui.foundation.SettingsAction
import io.git.luolix.topposegears.valkyrie.ui.foundation.TopAppBar
import io.git.luolix.topposegears.valkyrie.ui.foundation.VerticalSpacer
import io.git.luolix.topposegears.valkyrie.ui.foundation.WeightSpacer
import io.git.luolix.topposegears.valkyrie.ui.foundation.dashedBorder
import io.git.luolix.topposegears.valkyrie.ui.foundation.disabled
import io.git.luolix.topposegears.valkyrie.ui.foundation.icons.AddFile
import io.git.luolix.topposegears.valkyrie.ui.foundation.icons.ValkyrieIcons
import io.git.luolix.topposegears.valkyrie.ui.foundation.onPasteEvent
import io.git.luolix.topposegears.valkyrie.ui.foundation.rememberMutableState
import io.git.luolix.topposegears.valkyrie.ui.foundation.theme.PreviewTheme
import io.git.luolix.topposegears.valkyrie.ui.platform.ClipboardDataType
import io.git.luolix.topposegears.valkyrie.ui.platform.Os
import io.git.luolix.topposegears.valkyrie.ui.platform.pasteFromClipboard
import io.git.luolix.topposegears.valkyrie.ui.platform.picker.rememberDirectoryPicker
import io.git.luolix.topposegears.valkyrie.ui.platform.picker.rememberMultipleFilesPicker
import io.git.luolix.topposegears.valkyrie.ui.platform.rememberCurrentOs
import io.git.luolix.topposegears.valkyrie.ui.platform.rememberMultiSelectDragAndDropHandler
import io.git.luolix.topposegears.valkyrie.ui.screen.mode.iconpack.conversion.PickerEvent
import io.git.luolix.topposegears.valkyrie.ui.screen.mode.iconpack.conversion.PickerEvent.PickDirectory
Expand All @@ -43,20 +65,52 @@ import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlinx.coroutines.launch

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun IconPackPickerStateUi(
modifier: Modifier = Modifier,
onPickerEvent: (PickerEvent) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
openSettings: () -> Unit,
) {
val scope = rememberCoroutineScope()

val multipleFilePicker = rememberMultipleFilesPicker()
val directoryPicker = rememberDirectoryPicker()

val focusRequester = remember { FocusRequester() }

Column(
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.focusRequester(focusRequester)
.focusProperties { exit = { focusRequester } }
.focusable()
.onPointerEvent(PointerEventType.Enter) {
focusRequester.requestFocus()
}
.onPointerEvent(PointerEventType.Exit) {
focusRequester.freeFocus()
}
.onPasteEvent {
when (val clipboardData = pasteFromClipboard()) {
is ClipboardDataType.Files -> {
onPickerEvent(PickFiles(paths = clipboardData.paths))
}
is ClipboardDataType.Text -> {
onPickerEvent(PickerEvent.ClipboardText(clipboardData.text))
}
null -> {}
}
},
horizontalAlignment = Alignment.CenterHorizontally,
) {
TopAppBar {
BackAction(onBack = onBack)
AppBarTitle(title = "IconPack generation")
WeightSpacer()
SettingsAction(openSettings = openSettings)
}
WeightSpacer(weight = 0.3f)
SelectableState(
onSelectPath = { paths ->
Expand Down Expand Up @@ -93,6 +147,10 @@ fun IconPackPickerStateUi(
)
WeightSpacer(weight = 0.7f)
}

LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}

@OptIn(ExperimentalLayoutApi::class)
Expand All @@ -105,17 +163,35 @@ private fun SelectableState(
val dragAndDropHandler = rememberMultiSelectDragAndDropHandler(onDrop = onSelectPath)
val isDragging by rememberMutableState(dragAndDropHandler.isDragging) { dragAndDropHandler.isDragging }

val os = rememberCurrentOs()

DragAndDropBox(isDragging = isDragging) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = ValkyrieIcons.AddFile,
contentDescription = null,
)
CenterVerticalRow {
Icon(
imageVector = ValkyrieIcons.AddFile,
contentDescription = null,
)
Text(
modifier = Modifier.padding(8.dp),
text = "Drag & drop",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall,
)
}
Text(
modifier = Modifier.padding(8.dp),
text = "Drag & drop\n\nor",
text = when (os) {
Os.MacOS -> "Cmd+V to paste from clipboard"
else -> "Ctrl+V to paste from clipboard"
},
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall,
color = LocalContentColor.current.disabled(),
style = MaterialTheme.typography.labelSmall,
)
VerticalSpacer(16.dp)
Text(
text = "or",
style = MaterialTheme.typography.labelMedium,
)
FlowRow(
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -169,5 +245,9 @@ private fun DragAndDropBox(
@Preview
@Composable
private fun PickerStatePreview() = PreviewTheme {
IconPackPickerStateUi(onPickerEvent = {})
IconPackPickerStateUi(
onPickerEvent = {},
onBack = {},
openSettings = {},
)
}

0 comments on commit 82e3135

Please sign in to comment.