diff --git a/macOS/PostEditor/MacEditorTextView.swift b/macOS/PostEditor/MacEditorTextView.swift index 6bdac42..852c38a 100644 --- a/macOS/PostEditor/MacEditorTextView.swift +++ b/macOS/PostEditor/MacEditorTextView.swift @@ -1,203 +1,203 @@ // 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 font: NSFont? = NSFont(name: PostAppearance.serif.rawValue, size: 17) 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 = 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.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 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 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 400bc3c..73a9acd 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -1,100 +1,98 @@ 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 + @State private var updatingFromServer: Bool = false var body: some View { PostTextEditingView( post: post, - updatingTitleFromServer: $updatingTitleFromServer, - updatingBodyFromServer: $updatingBodyFromServer + updatingFromServer: $updatingFromServer ) .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 c2864c0..3b7115b 100644 --- a/macOS/PostEditor/PostTextEditingView.swift +++ b/macOS/PostEditor/PostTextEditingView.swift @@ -1,124 +1,133 @@ import SwiftUI struct PostTextEditingView: View { @ObservedObject var post: WFAPost - @Binding var updatingTitleFromServer: Bool - @Binding var updatingBodyFromServer: Bool - @State private var isHovering: Bool = false + @Binding var updatingFromServer: Bool @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)) // } 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: combinedText.isEmpty, - isEditable: true, - font: NSFont(name: appearance.rawValue, size: 17), - onEditingChanged: onEditingChanged, - onCommit: onCommit, - onTextChange: onTextChange - ) + if post.appearance == "sans" { + MacEditorTextView( + text: $combinedText, + isFirstResponder: combinedText.isEmpty, + isEditable: true, + font: NSFont(name: "OpenSans-Regular", size: 17), + onEditingChanged: onEditingChanged, + onCommit: onCommit, + onTextChange: onTextChange + ) + } else if post.appearance == "wrap" || post.appearance == "mono" || post.appearance == "code" { + MacEditorTextView( + text: $combinedText, + isFirstResponder: combinedText.isEmpty, + isEditable: true, + font: NSFont(name: "Hack-Regular", size: 17), + onEditingChanged: onEditingChanged, + onCommit: onCommit, + onTextChange: onTextChange + ) + } else { + MacEditorTextView( + text: $combinedText, + isFirstResponder: combinedText.isEmpty, + isEditable: true, + font: NSFont(name: "Lora-Regular", 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[..