diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorScreen.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorScreen.kt index ac01cc9..1eb70a0 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorScreen.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorScreen.kt @@ -101,6 +101,9 @@ fun EditorScreen( viewModel.onStrokesChanged = { canvasView.setStrokes(viewModel.strokes.value) } + viewModel.onSelectionChanged = { ids -> + canvasView.setSelection(ids) + } } Column(modifier = Modifier.fillMaxSize()) { diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorViewModel.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorViewModel.kt index 6998986..ac31476 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorViewModel.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/EditorViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import net.metacircular.engpad.data.db.toBlob +import net.metacircular.engpad.data.db.toFloatArray import net.metacircular.engpad.data.model.Page import net.metacircular.engpad.data.model.PageSize import net.metacircular.engpad.data.model.Stroke @@ -43,6 +44,10 @@ class EditorViewModel( private val _canPaste = MutableStateFlow(false) val canPaste: StateFlow = _canPaste private var clipboard = emptyList() + private var clipboardIsCut = false + + // Callback to update the canvas view's selection + var onSelectionChanged: ((Set) -> Unit)? = null // Page navigation private val _pages = MutableStateFlow>(emptyList()) @@ -211,35 +216,62 @@ class EditorViewModel( fun cutSelection() { clipboard = _strokes.value.filter { it.id in selectedIds } + clipboardIsCut = true _canPaste.value = clipboard.isNotEmpty() deleteSelection() } fun copyToClipboard() { clipboard = _strokes.value.filter { it.id in selectedIds } + clipboardIsCut = false _canPaste.value = clipboard.isNotEmpty() clearSelection() } fun paste() { if (clipboard.isEmpty()) return + val isCut = clipboardIsCut + // Offset copies so they're visually distinct from originals + val offsetStrokes = if (isCut) { + clipboard // Cut: paste at same position + } else { + clipboard.map { stroke -> + val points = stroke.pointData.toFloatArray() + val shifted = FloatArray(points.size) + var i = 0 + while (i < points.size - 1) { + shifted[i] = points[i] + PASTE_OFFSET_PT + shifted[i + 1] = points[i + 1] + PASTE_OFFSET_PT + i += 2 + } + stroke.copy(pointData = shifted.toBlob()) + } + } viewModelScope.launch { val pageId = _currentPageId.value undoManager.perform( CopyStrokesAction( - strokes = clipboard, + strokes = offsetStrokes, pageId = pageId, repository = pageRepository, onExecute = { copies -> _strokes.value = _strokes.value + copies - copies.forEach { onStrokeAdded?.invoke(it) } + onStrokesChanged?.invoke() + // Select the pasted strokes + val newIds = copies.map { it.id }.toSet() + selectedIds = newIds + _hasSelection.value = true + onSelectionChanged?.invoke(newIds) }, onUndo = { ids -> _strokes.value = _strokes.value.filter { it.id !in ids } ids.forEach { onStrokeRemoved?.invoke(it) } + clearSelection() }, ) ) + // After cut+paste, subsequent pastes should copy (not cut again) + if (isCut) clipboardIsCut = false } } @@ -334,6 +366,11 @@ class EditorViewModel( viewModelScope.launch { undoManager.redo() } } + companion object { + /** Offset for copy+paste in canonical points (~10mm diagonal). */ + private const val PASTE_OFFSET_PT = 120f + } + class Factory( private val notebookId: Long, private val pageSize: PageSize, diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt index a87655f..2f62483 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt @@ -881,6 +881,12 @@ class PadCanvasView(context: Context) : View(context) { fun getSelectedStrokeIds(): Set = selectedStrokeIds.toSet() + fun setSelection(ids: Set) { + selectedStrokeIds.clear() + selectedStrokeIds.addAll(ids) + invalidate() + } + // --- Eraser --- private fun handleEraserInput(event: MotionEvent): Boolean {