Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow continuous copy paste from clipboard #353

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

Expand Down Expand Up @@ -67,17 +59,3 @@ fun Modifier.blendMode(blendMode: BlendMode): Modifier {
}
}
}

fun Modifier.onPasteEvent(callback: () -> Unit): Modifier {
return this
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown && keyEvent.isCtrlV()) {
callback()
true
} else {
false
}
}
}

private fun KeyEvent.isCtrlV(): Boolean = (isCtrlPressed || isMetaPressed) && key == Key.V
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -36,8 +35,6 @@ 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.navArgsOrNull
import com.composegears.tiamat.navController
Expand Down Expand Up @@ -123,39 +120,30 @@ private fun IconPackConversionUi(
openSettings: () -> Unit,
onPickEvent: (PickerEvent) -> Unit,
updatePack: (BatchIcon, String) -> Unit,
onDeleteIcon: (IconName) -> Unit,
onDeleteIcon: (IconId) -> Unit,
onReset: () -> Unit,
onPreviewClick: (IconName) -> Unit,
onPreviewClick: (BatchIcon.Valid) -> Unit,
onExport: () -> Unit,
onRenameIcon: (BatchIcon, IconName) -> Unit,
) {
var isVisible by rememberSaveable { mutableStateOf(true) }
var isExportButtonVisible by rememberSaveable { mutableStateOf(true) }

val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (available.y < -1) {
isVisible = false
isExportButtonVisible = false
}
if (available.y > 1) {
isVisible = true
isExportButtonVisible = true
}

return Offset.Zero
}
}
}

val focusManager = LocalFocusManager.current

Box(
modifier = Modifier
.pointerInput(state) {
if (state is IconPackCreationState) {
detectTapGestures(onTap = { focusManager.clearFocus() })
}
},
) {
Box {
Column(modifier = Modifier.fillMaxSize()) {
AnimatedContent(
modifier = Modifier.fillMaxSize(),
Expand Down Expand Up @@ -191,6 +179,8 @@ private fun IconPackConversionUi(
modifier = Modifier.nestedScroll(nestedScrollConnection),
state = current,
previewType = previewType,
onScrollUnavailable = { isExportButtonVisible = true },
onPasteEvent = onPickEvent,
onDeleteIcon = onDeleteIcon,
onUpdatePack = updatePack,
onPreviewClick = onPreviewClick,
Expand All @@ -208,7 +198,7 @@ private fun IconPackConversionUi(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp),
visible = isVisible,
visible = isExportButtonVisible,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ sealed interface IconPackConversionState {
}

sealed interface BatchIcon {
val id: String
val id: IconId
val iconName: IconName

data class Broken(
override val id: String = Uuid.random(),
override val id: IconId = IconId(id = Uuid.random()),
override val iconName: IconName,
) : BatchIcon

data class Valid(
override val id: String = Uuid.random(),
override val id: IconId = IconId(id = Uuid.random()),
override val iconName: IconName,
val iconPack: IconPack,
val iconType: IconType,
Expand All @@ -41,6 +41,9 @@ sealed interface BatchIcon {
@JvmInline
value class IconName(val value: String)

@JvmInline
value class IconId(val id: String)

sealed interface IconPack {
val iconPackage: String
val currentNestedPack: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion
import com.composegears.tiamat.Saveable
import com.composegears.tiamat.SavedState
import com.composegears.tiamat.TiamatViewModel
import io.github.composegears.valkyrie.extensions.cast
import io.github.composegears.valkyrie.extensions.safeAs
import io.github.composegears.valkyrie.extensions.writeToKt
import io.github.composegears.valkyrie.generator.imagevector.ImageVectorGenerator
import io.github.composegears.valkyrie.generator.imagevector.ImageVectorGeneratorConfig
Expand Down Expand Up @@ -43,6 +43,8 @@ class IconPackConversionViewModel(
private val _events = MutableSharedFlow<ConversionEvent>()
val events = _events.asSharedFlow()

private var clipboardIconCounter = 0

init {
val restoredState = savedState?.getOrNull<List<BatchIcon>>(key = "icons")

Expand All @@ -67,11 +69,16 @@ class IconPackConversionViewModel(
}
}
}

savedState?.getOrNull<Int>(key = "clipboardIconCounter")?.also { clipboardIconCounter = it }
}

override fun saveToSaveState(): SavedState {
return when (val state = _state.value) {
is BatchProcessing.IconPackCreationState -> mapOf("icons" to state.icons)
is BatchProcessing.IconPackCreationState -> mapOf(
"icons" to state.icons,
"clipboardIconCounter" to clipboardIconCounter,
)
else -> mapOf("icons" to emptyList<List<BatchIcon>>())
}
}
Expand All @@ -84,15 +91,15 @@ class IconPackConversionViewModel(
}
}

fun deleteIcon(iconName: IconName) = viewModelScope.launch(Dispatchers.Default) {
fun deleteIcon(iconId: IconId) = viewModelScope.launch(Dispatchers.Default) {
_state.updateState {
when (this) {
is BatchProcessing.IconPackCreationState -> {
val iconsToProcess = icons.filter { it.iconName != iconName }
val iconsToProcess = icons.filter { it.id != iconId }

if (iconsToProcess.isEmpty()) {
_state.updateState { IconsPickering }
this
clipboardIconCounter = 0
IconsPickering
} else {
copy(
icons = iconsToProcess,
Expand All @@ -111,7 +118,7 @@ class IconPackConversionViewModel(
is BatchProcessing.IconPackCreationState -> {
copy(
icons = icons.map { icon ->
if (icon.iconName == batchIcon.iconName && icon is BatchIcon.Valid) {
if (icon.id == batchIcon.id && icon is BatchIcon.Valid) {
icon.copy(
iconPack = when (icon.iconPack) {
is IconPack.Nested -> icon.iconPack.copy(currentNestedPack = nestedPack)
Expand All @@ -129,18 +136,11 @@ class IconPackConversionViewModel(
}
}

fun showPreview(iconName: IconName) = viewModelScope.launch(Dispatchers.Default) {
val icons = when (val state = _state.value) {
is BatchProcessing.IconPackCreationState -> state.icons
else -> return@launch
}

val icon = icons.first { it.iconName == iconName }.cast<BatchIcon.Valid>()

fun showPreview(icon: BatchIcon.Valid) = viewModelScope.launch(Dispatchers.Default) {
val settings = inMemorySettings.current
val output = ImageVectorGenerator.convert(
vector = icon.irImageVector,
iconName = iconName.value,
iconName = icon.iconName.value,
config = ImageVectorGeneratorConfig(
packageName = icon.iconPack.iconPackage,
iconPackPackage = settings.iconPackPackage,
Expand Down Expand Up @@ -233,7 +233,7 @@ class IconPackConversionViewModel(
when (this) {
is BatchProcessing.IconPackCreationState -> {
val icons = icons.map { icon ->
if (icon.iconName == batchIcon.iconName) {
if (icon.id == batchIcon.id) {
when (icon) {
is BatchIcon.Broken -> icon
is BatchIcon.Valid -> icon.copy(iconName = newName)
Expand All @@ -254,10 +254,15 @@ class IconPackConversionViewModel(

fun reset() {
_state.updateState { IconsPickering }
clipboardIconCounter = 0
}

private fun processText(text: String) = viewModelScope.launch(Dispatchers.Default) {
val iconName = "IconName"
val iconName = when (clipboardIconCounter) {
0 -> "IconName"
else -> "IconName_$clipboardIconCounter"
}
clipboardIconCounter++

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

Expand All @@ -271,7 +276,8 @@ class IconPackConversionViewModel(
)
}
_state.updateState {
val icons = listOf(icon)
val existingIcons = safeAs<BatchProcessing.IconPackCreationState>()?.icons.orEmpty()
val icons = existingIcons + icon

BatchProcessing.IconPackCreationState(
icons = icons,
Expand Down Expand Up @@ -328,7 +334,10 @@ class IconPackConversionViewModel(

private fun List<BatchIcon>.isAllIconsValid() = isNotEmpty() &&
all { it is BatchIcon.Valid } &&
all { it.iconName.value.isNotEmpty() && !it.iconName.value.contains(" ") }
all { it.iconName.value.isNotEmpty() && !it.iconName.value.contains(" ") } &&
hasNoDuplicates()

private fun List<BatchIcon>.hasNoDuplicates() = map { it.iconName.value }.toSet().size == size
}

sealed interface ConversionEvent {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.github.composegears.valkyrie.ui.screen.mode.iconpack.conversion.ui

import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import io.github.composegears.valkyrie.ui.platform.ClipboardDataType
import io.github.composegears.valkyrie.ui.platform.pasteFromClipboard

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ClipboardEventColumn(
horizontalAlignment: Alignment.Horizontal,
onPaste: (ClipboardDataType) -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
val focusRequester = remember { FocusRequester() }

Column(
modifier = modifier
.fillMaxSize()
.focusRequester(focusRequester)
.focusProperties { exit = { focusRequester } }
.focusable()
.onPointerEvent(PointerEventType.Enter) {
focusRequester.requestFocus()
}
.onPointerEvent(PointerEventType.Exit) {
focusRequester.freeFocus()
}
.onPasteEvent {
pasteFromClipboard()?.let(onPaste)
},
content = content,
horizontalAlignment = horizontalAlignment,
)

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

private fun Modifier.onPasteEvent(callback: () -> Unit): Modifier {
return this
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown && keyEvent.isCtrlV()) {
callback()
true
} else {
false
}
}
}

private fun KeyEvent.isCtrlV(): Boolean = (isCtrlPressed || isMetaPressed) && key == Key.V
Loading
Loading