diff --git a/Sources/SaplingApp/SaplingApp.swift b/Sources/SaplingApp/SaplingApp.swift index dc12d9d..09859fa 100644 --- a/Sources/SaplingApp/SaplingApp.swift +++ b/Sources/SaplingApp/SaplingApp.swift @@ -9,8 +9,16 @@ import SaplingEditor import SaplingRenderer import SaplingUI +#if os(macOS) +import AppKit +#endif + @main struct SaplingApplication: App { + #if os(macOS) + @NSApplicationDelegateAdaptor(SaplingAppDelegate.self) private var appDelegate + #endif + @StateObject private var model = SaplingAppModel(dependencies: .live()) var body: some Scene { @@ -33,6 +41,15 @@ struct SaplingApplication: App { } } +#if os(macOS) +private final class SaplingAppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + } +} +#endif + @MainActor private final class SaplingAppModel: ObservableObject { @Published var workspace: Workspace diff --git a/Sources/SaplingEditor/HybridMarkdownEditor.swift b/Sources/SaplingEditor/HybridMarkdownEditor.swift index 29deb31..f9ad721 100644 --- a/Sources/SaplingEditor/HybridMarkdownEditor.swift +++ b/Sources/SaplingEditor/HybridMarkdownEditor.swift @@ -150,7 +150,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable { layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) - let textView = NSTextView(frame: scrollView.contentView.bounds, textContainer: textContainer) + let textView = EditorTextView(frame: scrollView.contentView.bounds, textContainer: textContainer) textView.autoresizingMask = [.width] textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height) textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) @@ -160,6 +160,8 @@ private struct NativeMarkdownTextView: NSViewRepresentable { textView.delegate = context.coordinator textView.string = text textView.isRichText = false + textView.isEditable = true + textView.isSelectable = true textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticDashSubstitutionEnabled = false textView.isAutomaticTextReplacementEnabled = false @@ -179,6 +181,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable { scrollView.editorTextView = textView scrollView.updateEditorInsets() context.coordinator.applyHybridAttributes(to: textView) + context.coordinator.requestInitialFocus(for: textView) return scrollView } @@ -199,6 +202,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable { } context.coordinator.applyHybridAttributes(to: textView) + context.coordinator.requestInitialFocus(for: textView) } final class Coordinator: NSObject, NSTextViewDelegate { @@ -206,6 +210,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable { private var programmaticUpdateDepth = 0 private var lastStyledText: String? private var lastStyledActiveLineIndex: Int? + private var didFocusTextView = false init(_ parent: NativeMarkdownTextView) { self.parent = parent @@ -263,6 +268,24 @@ private struct NativeMarkdownTextView: NSViewRepresentable { } } + func requestInitialFocus(for textView: NSTextView) { + guard !didFocusTextView else { return } + + DispatchQueue.main.async { [weak self, weak textView] in + guard let self, + let textView, + let window = textView.window, + !self.didFocusTextView + else { return } + + if window.firstResponder !== textView { + window.makeFirstResponder(textView) + } + + self.didFocusTextView = window.firstResponder === textView + } + } + func performProgrammaticUpdate(_ updates: () -> Void) { programmaticUpdateDepth += 1 defer { @@ -281,6 +304,12 @@ private struct NativeMarkdownTextView: NSViewRepresentable { } } +private final class EditorTextView: NSTextView { + override var acceptsFirstResponder: Bool { + true + } +} + private final class ComfortableEditorScrollView: NSScrollView { weak var editorTextView: NSTextView? @@ -323,6 +352,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable { textView.textContainerInset = UIEdgeInsets(top: 28, left: 24, bottom: 28, right: 24) textView.backgroundColor = .systemBackground context.coordinator.applyHybridAttributes(to: textView) + context.coordinator.requestInitialFocus(for: textView) return textView } @@ -334,6 +364,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable { } } context.coordinator.applyHybridAttributes(to: textView) + context.coordinator.requestInitialFocus(for: textView) } final class Coordinator: NSObject, UITextViewDelegate { @@ -341,6 +372,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable { private var programmaticUpdateDepth = 0 private var lastStyledText: String? private var lastStyledActiveLineIndex: Int? + private var didFocusTextView = false init(_ parent: NativeMarkdownTextView) { self.parent = parent @@ -395,6 +427,20 @@ private struct NativeMarkdownTextView: UIViewRepresentable { updates() } + func requestInitialFocus(for textView: UITextView) { + guard !didFocusTextView else { return } + + DispatchQueue.main.async { [weak self, weak textView] in + guard let self, + let textView, + textView.window != nil, + !self.didFocusTextView + else { return } + + self.didFocusTextView = textView.becomeFirstResponder() + } + } + private var isPerformingProgrammaticUpdate: Bool { programmaticUpdateDepth > 0 }