Improve paste behavior: selection retention and copy offset
Cut + paste: inserts at same position, selects the pasted strokes. Copy + paste: offsets pasted strokes by ~10mm diagonally to distinguish from originals, selects the new copies. Both operations leave the pasted strokes selected so they can be immediately moved, copied again, or deleted. After a cut+paste, subsequent pastes behave as copy+paste (no repeated cuts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,9 @@ fun EditorScreen(
|
||||
viewModel.onStrokesChanged = {
|
||||
canvasView.setStrokes(viewModel.strokes.value)
|
||||
}
|
||||
viewModel.onSelectionChanged = { ids ->
|
||||
canvasView.setSelection(ids)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
@@ -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<Boolean> = _canPaste
|
||||
private var clipboard = emptyList<Stroke>()
|
||||
private var clipboardIsCut = false
|
||||
|
||||
// Callback to update the canvas view's selection
|
||||
var onSelectionChanged: ((Set<Long>) -> Unit)? = null
|
||||
|
||||
// Page navigation
|
||||
private val _pages = MutableStateFlow<List<Page>>(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,
|
||||
|
||||
@@ -881,6 +881,12 @@ class PadCanvasView(context: Context) : View(context) {
|
||||
|
||||
fun getSelectedStrokeIds(): Set<Long> = selectedStrokeIds.toSet()
|
||||
|
||||
fun setSelection(ids: Set<Long>) {
|
||||
selectedStrokeIds.clear()
|
||||
selectedStrokeIds.addAll(ids)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// --- Eraser ---
|
||||
|
||||
private fun handleEraserInput(event: MotionEvent): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user