| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | import 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. |
| 7 | struct 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 | |