diff --git a/macOS/PostEditor/MacEditorTextView.swift b/macOS/PostEditor/MacEditorTextView.swift index a3ed56f..6bdac42 100644 --- a/macOS/PostEditor/MacEditorTextView.swift +++ b/macOS/PostEditor/MacEditorTextView.swift @@ -1,206 +1,203 @@ -/** -* Based on: -* -* MacEditorTextView -* Copyright (c) Thiago Holanda 2020 -* https://twitter.com/tholanda -* -* MIT license -* -* See: https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 -*/ +// Based on: +// +// MacEditorTextView +// Copyright (c) Thiago Holanda 2020 +// https://twitter.com/tholanda +// +// MIT license +// +// See: https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 import Combine import SwiftUI struct MacEditorTextView: NSViewRepresentable { @Binding var text: String var isFirstResponder: Bool = false var isEditable: Bool = true var font: NSFont? = .systemFont(ofSize: 14, weight: .regular) - - var onEditingChanged: () -> Void = {} - var onCommit : () -> Void = {} - var onTextChange : (String) -> Void = { _ in } - + + var onEditingChanged: () -> Void = {} + var onCommit: () -> Void = {} + var onTextChange: (String) -> Void = { _ in } + func makeCoordinator() -> Coordinator { Coordinator(self) } - + func makeNSView(context: Context) -> CustomTextView { let textView = CustomTextView( text: text, isEditable: isEditable, isFirstResponder: isFirstResponder, font: font ) textView.delegate = context.coordinator - + return textView } - + func updateNSView(_ view: CustomTextView, context: Context) { view.text = text view.selectedRanges = context.coordinator.selectedRanges } } // MARK: - Coordinator extension MacEditorTextView { - + class Coordinator: NSObject, NSTextViewDelegate { var parent: MacEditorTextView var selectedRanges: [NSValue] = [] var didBecomeFirstResponder: Bool = false - + init(_ parent: MacEditorTextView) { self.parent = parent } - + func textDidBeginEditing(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } - + self.parent.text = textView.string self.parent.onEditingChanged() } - + func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } - + self.parent.text = textView.string self.selectedRanges = textView.selectedRanges self.parent.onTextChange(textView.string) } - + func textDidEndEditing(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } - + self.parent.text = textView.string self.parent.onCommit() } } } // MARK: - CustomTextView final class CustomTextView: NSView { private var isFirstResponder: Bool private var isEditable: Bool private var font: NSFont? - + weak var delegate: NSTextViewDelegate? - + var text: String { didSet { textView.string = text } } - + var selectedRanges: [NSValue] = [] { didSet { guard selectedRanges.count > 0 else { return } - + textView.selectedRanges = selectedRanges } } - + private lazy var scrollView: NSScrollView = { let scrollView = NSScrollView() - scrollView.drawsBackground = true + scrollView.drawsBackground = false scrollView.borderType = .noBorder scrollView.hasVerticalScroller = true scrollView.hasHorizontalRuler = false scrollView.autoresizingMask = [.width, .height] scrollView.translatesAutoresizingMaskIntoConstraints = false - + return scrollView }() - + private lazy var textView: NSTextView = { let contentSize = scrollView.contentSize let textStorage = NSTextStorage() - - + let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) - - + let textContainer = NSTextContainer(containerSize: scrollView.frame.size) textContainer.widthTracksTextView = true textContainer.containerSize = NSSize( width: contentSize.width, height: CGFloat.greatestFiniteMagnitude ) - + layoutManager.addTextContainer(textContainer) - - - let textView = NSTextView(frame: .zero, textContainer: textContainer) - textView.autoresizingMask = .width - textView.backgroundColor = NSColor.textBackgroundColor - textView.delegate = self.delegate - textView.drawsBackground = true - textView.font = self.font - textView.isEditable = self.isEditable + + let textView = NSTextView(frame: .zero, textContainer: textContainer) + textView.autoresizingMask = .width + textView.delegate = self.delegate + textView.drawsBackground = false + textView.font = self.font + textView.isEditable = self.isEditable textView.isHorizontallyResizable = false - textView.isVerticallyResizable = true - textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) - textView.minSize = NSSize(width: 0, height: contentSize.height) - textView.textColor = NSColor.labelColor - + textView.isVerticallyResizable = true + textView.maxSize = NSSize( + width: CGFloat.greatestFiniteMagnitude, + height: CGFloat.greatestFiniteMagnitude + ) + textView.minSize = NSSize(width: 0, height: contentSize.height) + textView.textColor = NSColor.labelColor + return textView }() - + // MARK: - Init init(text: String, isEditable: Bool, isFirstResponder: Bool, font: NSFont?) { - self.font = font - self.isFirstResponder = isFirstResponder - self.isEditable = isEditable - self.text = text - + self.font = font + self.isFirstResponder = isFirstResponder + self.isEditable = isEditable + self.text = text + super.init(frame: .zero) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Life cycle - + override func viewWillDraw() { super.viewWillDraw() - + setupScrollViewConstraints() setupTextView() - + if isFirstResponder { self.window?.makeFirstResponder(self.textView) } } - + func setupScrollViewConstraints() { scrollView.translatesAutoresizingMaskIntoConstraints = false - + addSubview(scrollView) - + NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: topAnchor), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), scrollView.leadingAnchor.constraint(equalTo: leadingAnchor) ]) } - + func setupTextView() { scrollView.documentView = textView } } diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift index 6d2b238..400bc3c 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -1,99 +1,100 @@ import SwiftUI struct PostEditorView: View { private let bodyLineSpacing: CGFloat = 17 * 0.5 @EnvironmentObject var model: WriteFreelyModel @ObservedObject var post: WFAPost @State private var isHovering: Bool = false @State private var updatingTitleFromServer: Bool = false @State private var updatingBodyFromServer: Bool = false var body: some View { PostTextEditingView( post: post, updatingTitleFromServer: $updatingTitleFromServer, updatingBodyFromServer: $updatingBodyFromServer ) .padding() + .background(Color(NSColor.controlBackgroundColor)) .toolbar { ToolbarItem(placement: .status) { PostEditorStatusToolbarView(post: post) } ToolbarItem(placement: .primaryAction) { Button(action: { if model.account.isLoggedIn { publishPost() } else { let mainMenu = NSApplication.shared.mainMenu let appMenuItem = mainMenu?.item(withTitle: "WriteFreely") let prefsItem = appMenuItem?.submenu?.item(withTitle: "Preferences…") NSApplication.shared.sendAction(prefsItem!.action!, to: prefsItem?.target, from: nil) } }, label: { Image(systemName: "paperplane") }) .disabled(post.status == PostStatus.published.rawValue || post.body.count == 0) } } .onChange(of: post.hasNewerRemoteCopy, perform: { _ in if post.status == PostStatus.edited.rawValue && !post.hasNewerRemoteCopy { post.status = PostStatus.published.rawValue } }) .onDisappear(perform: { if post.title.count == 0 && post.body.count == 0 && post.status == PostStatus.local.rawValue && post.updatedDate == nil && post.postId == nil { DispatchQueue.main.async { model.posts.remove(post) } } else if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { LocalStorageManager().saveContext() } } }) } private func publishPost() { DispatchQueue.main.async { LocalStorageManager().saveContext() model.publish(post: post) } } } struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." testPost.createdDate = Date() testPost.appearance = "code" let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/macOS/PostEditor/PostTextEditingView.swift b/macOS/PostEditor/PostTextEditingView.swift index e3026c1..557e474 100644 --- a/macOS/PostEditor/PostTextEditingView.swift +++ b/macOS/PostEditor/PostTextEditingView.swift @@ -1,114 +1,124 @@ import SwiftUI struct PostTextEditingView: View { @ObservedObject var post: WFAPost @Binding var updatingTitleFromServer: Bool @Binding var updatingBodyFromServer: Bool @State private var isHovering: Bool = false @State private var appearance: PostAppearance = .serif @State private var combinedText = "" var body: some View { // VStack { // TextField("Title (optional)", text: $post.title) // .textFieldStyle(PlainTextFieldStyle()) // .padding(.horizontal, 4) // .font(.custom(appearance.rawValue, size: 26, relativeTo: .largeTitle)) // .onChange(of: post.title) { _ in // if post.status == PostStatus.published.rawValue && !updatingTitleFromServer { // post.status = PostStatus.edited.rawValue // } // if updatingTitleFromServer { // updatingTitleFromServer = false // } // } // .padding(4) // .background(Color(NSColor.controlBackgroundColor)) // .padding(.bottom) // ZStack(alignment: .topLeading) { // if post.body.count == 0 { // Text("Write…") // .foregroundColor(Color(NSColor.placeholderTextColor)) // .padding(.horizontal, 4) // .padding(.vertical, 2) // .font(.custom(appearance.rawValue, size: 17, relativeTo: .body)) // } // TextEditor(text: $post.body) // .font(.custom(appearance.rawValue, size: 17, relativeTo: .body)) // .opacity(post.body.count == 0 && !isHovering ? 0.0 : 1.0) // .onChange(of: post.body) { _ in // if post.status == PostStatus.published.rawValue && !updatingBodyFromServer { // post.status = PostStatus.edited.rawValue // } // if updatingBodyFromServer { // updatingBodyFromServer = false // } // } // .onHover(perform: { hovering in // self.isHovering = hovering // }) // } // .padding(4) // .background(Color(NSColor.controlBackgroundColor)) // } - MacEditorTextView( - text: $combinedText, - isFirstResponder: post.status == PostStatus.local.rawValue, - isEditable: true, - font: NSFont(name: appearance.rawValue, size: 17), - onEditingChanged: onEditingChanged, - onCommit: onCommit, - onTextChange: onTextChange - ) + ZStack(alignment: .topLeading) { + if combinedText.count == 0 { + Text("Write…") + .foregroundColor(Color(NSColor.placeholderTextColor)) + .padding(.horizontal, 5) + .font(.custom(appearance.rawValue, size: 17, relativeTo: .body)) + } + MacEditorTextView( + text: $combinedText, + isFirstResponder: post.status == PostStatus.local.rawValue, + isEditable: true, + font: NSFont(name: appearance.rawValue, size: 17), + onEditingChanged: onEditingChanged, + onCommit: onCommit, + onTextChange: onTextChange + ) + } + .background(Color(NSColor.controlBackgroundColor)) .onAppear(perform: { switch post.appearance { case "sans": self.appearance = .sans case "wrap", "mono", "code": self.appearance = .mono default: self.appearance = .serif } + print("Font: \(appearance.rawValue)") if post.title.isEmpty { self.combinedText = post.body } else { self.combinedText = "# \(post.title)\n\n\(post.body)" } }) } private func onEditingChanged() { print("onEditingChanged fired") } private func onTextChange(_ text: String) { print("onTextChange fired") extractTitle(text) } private func onCommit() { print("onCommit fired") } private func extractTitle(_ text: String) { var detectedTitle: String if text.hasPrefix("# ") { let endOfTitleIndex = text.firstIndex(of: "\n") ?? text.endIndex detectedTitle = String(text[..