oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import AppKit
3import SwiftUI
4
5/// Position the field editor's caret as part of gaining focus, before another
6/// keyboard event can replace the character that started cell editing.
7struct SheetCellEditor: NSViewRepresentable {
8 @Binding var text: String
9 let enabled: Bool
10 let label: String
11 let commit: (Bool) -> Void
12 let cancel: () -> Void
13 func makeCoordinator() -> Coordinator { Coordinator(self) }
14 func makeNSView(context: Context) -> CellTextField {
15 let field = CellTextField()
16 field.isBordered = false; field.drawsBackground = false
17 // The cell already draws its selection border around the full cell.
18 field.focusRingType = .none
19 field.font = .systemFont(ofSize: 13)
20 field.delegate = context.coordinator
21 field.stringValue = text
22 field.setAccessibilityLabel(label)
23 return field
24 }
25 func updateNSView(_ field: CellTextField, context: Context) {
26 context.coordinator.parent = self
27 let regainedEditing = enabled && !field.isEnabled
28 field.isEnabled = enabled
29 if field.stringValue != text {
30 field.stringValue = text
31 if let editor = field.currentEditor() as? NSTextView {
32 editor.setSelectedRange(NSRange(location: text.utf16.count, length: 0))
33 }
34 }
35 if regainedEditing { field.window?.makeFirstResponder(field) }
36 }
37 final class CellTextField: NSTextField {
38 override func viewDidMoveToWindow() {
39 super.viewDidMoveToWindow()
40 guard let window else { return }
41 // The representable must be attached before it can become responder.
42 DispatchQueue.main.async { [weak self, weak window] in
43 guard let self, let window, self.window === window, self.isEnabled else { return }
44 window.makeFirstResponder(self)
45 }
46 }
47 override func becomeFirstResponder() -> Bool {
48 let accepted = super.becomeFirstResponder()
49 if accepted, let editor = currentEditor() as? NSTextView {
50 editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0))
51 }
52 return accepted
53 }
54 }
55 @MainActor final class Coordinator: NSObject, NSTextFieldDelegate {
56 var parent: SheetCellEditor
57 init(_ parent: SheetCellEditor) { self.parent = parent }
58 func controlTextDidChange(_ notification: Notification) {
59 guard let field = notification.object as? NSTextField else { return }
60 parent.text = field.stringValue
61 }
62 func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool {
63 switch selector {
64 case #selector(NSResponder.insertNewline(_:)): parent.commit(false)
65 case #selector(NSResponder.insertTab(_:)): parent.commit(true)
66 case #selector(NSResponder.cancelOperation(_:)): parent.cancel()
67 default: return false
68 }
69 return true
70 }
71 }
72}
73#endif
74