fix(editor): activate text editor for keyboard input
This commit is contained in:
parent
ce55b9a2ce
commit
a010cb19be
2 changed files with 64 additions and 1 deletions
|
|
@ -9,8 +9,16 @@ import SaplingEditor
|
||||||
import SaplingRenderer
|
import SaplingRenderer
|
||||||
import SaplingUI
|
import SaplingUI
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct SaplingApplication: App {
|
struct SaplingApplication: App {
|
||||||
|
#if os(macOS)
|
||||||
|
@NSApplicationDelegateAdaptor(SaplingAppDelegate.self) private var appDelegate
|
||||||
|
#endif
|
||||||
|
|
||||||
@StateObject private var model = SaplingAppModel(dependencies: .live())
|
@StateObject private var model = SaplingAppModel(dependencies: .live())
|
||||||
|
|
||||||
var body: some Scene {
|
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
|
@MainActor
|
||||||
private final class SaplingAppModel: ObservableObject {
|
private final class SaplingAppModel: ObservableObject {
|
||||||
@Published var workspace: Workspace
|
@Published var workspace: Workspace
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
layoutManager.addTextContainer(textContainer)
|
layoutManager.addTextContainer(textContainer)
|
||||||
textStorage.addLayoutManager(layoutManager)
|
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.autoresizingMask = [.width]
|
||||||
textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height)
|
textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height)
|
||||||
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||||
|
|
@ -160,6 +160,8 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
textView.string = text
|
textView.string = text
|
||||||
textView.isRichText = false
|
textView.isRichText = false
|
||||||
|
textView.isEditable = true
|
||||||
|
textView.isSelectable = true
|
||||||
textView.isAutomaticQuoteSubstitutionEnabled = false
|
textView.isAutomaticQuoteSubstitutionEnabled = false
|
||||||
textView.isAutomaticDashSubstitutionEnabled = false
|
textView.isAutomaticDashSubstitutionEnabled = false
|
||||||
textView.isAutomaticTextReplacementEnabled = false
|
textView.isAutomaticTextReplacementEnabled = false
|
||||||
|
|
@ -179,6 +181,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
scrollView.editorTextView = textView
|
scrollView.editorTextView = textView
|
||||||
scrollView.updateEditorInsets()
|
scrollView.updateEditorInsets()
|
||||||
context.coordinator.applyHybridAttributes(to: textView)
|
context.coordinator.applyHybridAttributes(to: textView)
|
||||||
|
context.coordinator.requestInitialFocus(for: textView)
|
||||||
return scrollView
|
return scrollView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,6 +202,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
context.coordinator.applyHybridAttributes(to: textView)
|
context.coordinator.applyHybridAttributes(to: textView)
|
||||||
|
context.coordinator.requestInitialFocus(for: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Coordinator: NSObject, NSTextViewDelegate {
|
final class Coordinator: NSObject, NSTextViewDelegate {
|
||||||
|
|
@ -206,6 +210,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
private var programmaticUpdateDepth = 0
|
private var programmaticUpdateDepth = 0
|
||||||
private var lastStyledText: String?
|
private var lastStyledText: String?
|
||||||
private var lastStyledActiveLineIndex: Int?
|
private var lastStyledActiveLineIndex: Int?
|
||||||
|
private var didFocusTextView = false
|
||||||
|
|
||||||
init(_ parent: NativeMarkdownTextView) {
|
init(_ parent: NativeMarkdownTextView) {
|
||||||
self.parent = parent
|
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) {
|
func performProgrammaticUpdate(_ updates: () -> Void) {
|
||||||
programmaticUpdateDepth += 1
|
programmaticUpdateDepth += 1
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -281,6 +304,12 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class EditorTextView: NSTextView {
|
||||||
|
override var acceptsFirstResponder: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class ComfortableEditorScrollView: NSScrollView {
|
private final class ComfortableEditorScrollView: NSScrollView {
|
||||||
weak var editorTextView: NSTextView?
|
weak var editorTextView: NSTextView?
|
||||||
|
|
||||||
|
|
@ -323,6 +352,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
|
||||||
textView.textContainerInset = UIEdgeInsets(top: 28, left: 24, bottom: 28, right: 24)
|
textView.textContainerInset = UIEdgeInsets(top: 28, left: 24, bottom: 28, right: 24)
|
||||||
textView.backgroundColor = .systemBackground
|
textView.backgroundColor = .systemBackground
|
||||||
context.coordinator.applyHybridAttributes(to: textView)
|
context.coordinator.applyHybridAttributes(to: textView)
|
||||||
|
context.coordinator.requestInitialFocus(for: textView)
|
||||||
return textView
|
return textView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,6 +364,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.coordinator.applyHybridAttributes(to: textView)
|
context.coordinator.applyHybridAttributes(to: textView)
|
||||||
|
context.coordinator.requestInitialFocus(for: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Coordinator: NSObject, UITextViewDelegate {
|
final class Coordinator: NSObject, UITextViewDelegate {
|
||||||
|
|
@ -341,6 +372,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
|
||||||
private var programmaticUpdateDepth = 0
|
private var programmaticUpdateDepth = 0
|
||||||
private var lastStyledText: String?
|
private var lastStyledText: String?
|
||||||
private var lastStyledActiveLineIndex: Int?
|
private var lastStyledActiveLineIndex: Int?
|
||||||
|
private var didFocusTextView = false
|
||||||
|
|
||||||
init(_ parent: NativeMarkdownTextView) {
|
init(_ parent: NativeMarkdownTextView) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
@ -395,6 +427,20 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
|
||||||
updates()
|
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 {
|
private var isPerformingProgrammaticUpdate: Bool {
|
||||||
programmaticUpdateDepth > 0
|
programmaticUpdateDepth > 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue