| 1 | import Foundation |
| 2 | |
| 3 | #if os(macOS) |
| 4 | import AppKit |
| 5 | #else |
| 6 | import UIKit |
| 7 | #endif |
| 8 | @MainActor extension SheetModel { |
| 9 | func clearCopyProvenance() { |
| 10 | copied = nil |
| 11 | copiedText = "" |
| 12 | } |
| 13 | func copy() { |
| 14 | let range = selection |
| 15 | let rows = (range.start.r...range.end.r).map { r in |
| 16 | (range.start.c...range.end.c).map { c in |
| 17 | CellWrite(r: r, c: c, raw: cells[Coordinate(r: r, c: c)]?.raw ?? "") |
| 18 | } |
| 19 | } |
| 20 | copied = rows |
| 21 | copiedText = TSV.encode(rows.map { $0.map(\.raw) }) |
| 22 | #if os(macOS) |
| 23 | NSPasteboard.general.clearContents() |
| 24 | NSPasteboard.general.setString(copiedText, forType: .string) |
| 25 | #else |
| 26 | UIPasteboard.general.string = copiedText |
| 27 | #endif |
| 28 | } |
| 29 | func paste() async { |
| 30 | guard !dirty && !busy else { |
| 31 | error = "Save or discard your cell edit first." |
| 32 | return |
| 33 | } |
| 34 | #if os(macOS) |
| 35 | let text = NSPasteboard.general.string(forType: .string) |
| 36 | #else |
| 37 | let text = UIPasteboard.general.string |
| 38 | #endif |
| 39 | guard let text else { |
| 40 | error = "The clipboard has no text cells." |
| 41 | return |
| 42 | } |
| 43 | let values = TSV.decode(text) |
| 44 | guard values.reduce(0, { $0 + $1.count }) <= 500 else { |
| 45 | error = "Paste supports up to 500 cells at once." |
| 46 | return |
| 47 | } |
| 48 | var writes: [CellWrite] = [] |
| 49 | for (r, row) in values.enumerated() { |
| 50 | for (c, raw) in row.enumerated() { |
| 51 | let dest = Coordinate(r: selection.start.r + r, c: selection.start.c + c) |
| 52 | guard dest.r < rows && dest.c < cols else { |
| 53 | error = "This paste extends beyond the visible sheet. Nothing was changed." |
| 54 | return |
| 55 | } |
| 56 | let source = |
| 57 | text == copiedText && copied?.indices.contains(r) == true |
| 58 | && copied![r].indices.contains(c) ? copied![r][c] : nil |
| 59 | writes.append(CellWrite(r: dest.r, c: dest.c, raw: raw, fromR: source?.r, fromC: source?.c)) |
| 60 | } |
| 61 | } |
| 62 | _ = await write(writes) |
| 63 | } |
| 64 | func clear() async { |
| 65 | guard !dirty && !busy else { |
| 66 | error = "Save or discard your cell edit first." |
| 67 | return |
| 68 | } |
| 69 | _ = await write( |
| 70 | selection.coordinates.filter { !(cells[$0]?.raw ?? "").isEmpty }.map { |
| 71 | CellWrite(r: $0.r, c: $0.c, raw: "") |
| 72 | }) |
| 73 | } |
| 74 | func fill(down: Bool) async { |
| 75 | guard !dirty && !busy else { |
| 76 | error = "Save or discard your cell edit first." |
| 77 | return |
| 78 | } |
| 79 | _ = await write(fillWrites(down: down)) |
| 80 | } |
| 81 | func fillWrites(down: Bool) -> [CellWrite] { |
| 82 | let source = selection.start |
| 83 | let destinations = |
| 84 | selection.count == 1 |
| 85 | ? [ |
| 86 | Coordinate( |
| 87 | r: min(source.r + (down ? 1 : 0), rows - 1), c: min(source.c + (down ? 0 : 1), cols - 1)) |
| 88 | ] |
| 89 | : selection.coordinates |
| 90 | return destinations.compactMap { point -> CellWrite? in |
| 91 | if source == point { return nil } |
| 92 | return CellWrite( |
| 93 | r: point.r, c: point.c, raw: cells[source]?.raw ?? "", fromR: source.r, fromC: source.c) |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |