From 2e8ff0856598453d608fa546d4d5aa319ea5c2bc Mon Sep 17 00:00:00 2001 From: Anthony Ingle Date: Sat, 30 Apr 2022 03:41:15 -0400 Subject: [PATCH] New implementation of NSScrollView, cleaned up some --- Calculator.xcodeproj/project.pbxproj | 8 +- Calculator/AppDelegate.swift | 4 + Calculator/CalculatorView.swift | 4 +- Calculator/CustomMacTextView.swift | 119 ++++++++++++ Calculator/MacEditorTextView.swift | 268 --------------------------- 5 files changed, 129 insertions(+), 274 deletions(-) create mode 100644 Calculator/CustomMacTextView.swift delete mode 100644 Calculator/MacEditorTextView.swift diff --git a/Calculator.xcodeproj/project.pbxproj b/Calculator.xcodeproj/project.pbxproj index 44d0b10..b11a288 100644 --- a/Calculator.xcodeproj/project.pbxproj +++ b/Calculator.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 9218BABD280DB3E90068F3AF /* MacEditorTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9218BABC280DB3E90068F3AF /* MacEditorTextView.swift */; }; + 925E9140281D02AC0027E183 /* CustomMacTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925E913F281D02AC0027E183 /* CustomMacTextView.swift */; }; 928D96C6280A8F9300B6CE2E /* ButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928D96C5280A8F9300B6CE2E /* ButtonView.swift */; }; 928D96C8280AA58D00B6CE2E /* CalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928D96C7280AA58D00B6CE2E /* CalculatorView.swift */; }; 928D96CA280AA5E400B6CE2E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928D96C9280AA5E400B6CE2E /* SettingsView.swift */; }; @@ -22,7 +22,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 9218BABC280DB3E90068F3AF /* MacEditorTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacEditorTextView.swift; sourceTree = ""; }; + 925E913F281D02AC0027E183 /* CustomMacTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMacTextView.swift; sourceTree = ""; }; 928D96C5280A8F9300B6CE2E /* ButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonView.swift; sourceTree = ""; }; 928D96C7280AA58D00B6CE2E /* CalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorView.swift; sourceTree = ""; }; 928D96C9280AA5E400B6CE2E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -70,7 +70,7 @@ isa = PBXGroup; children = ( 92F447832808C1B600EC45D5 /* Info.plist */, - 9218BABC280DB3E90068F3AF /* MacEditorTextView.swift */, + 925E913F281D02AC0027E183 /* CustomMacTextView.swift */, 92F4476B28089FEE00EC45D5 /* CalculatorApp.swift */, 92F447842808C25800EC45D5 /* AppDelegate.swift */, 92F4476D28089FEE00EC45D5 /* ContentView.swift */, @@ -175,10 +175,10 @@ 928D96CA280AA5E400B6CE2E /* SettingsView.swift in Sources */, 928D96C8280AA58D00B6CE2E /* CalculatorView.swift in Sources */, 928D96C6280A8F9300B6CE2E /* ButtonView.swift in Sources */, + 925E9140281D02AC0027E183 /* CustomMacTextView.swift in Sources */, 92F4476E28089FEE00EC45D5 /* ContentView.swift in Sources */, 92F447852808C25800EC45D5 /* AppDelegate.swift in Sources */, 92F4476C28089FEE00EC45D5 /* CalculatorApp.swift in Sources */, - 9218BABD280DB3E90068F3AF /* MacEditorTextView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Calculator/AppDelegate.swift b/Calculator/AppDelegate.swift index 32d61c6..af106e8 100644 --- a/Calculator/AppDelegate.swift +++ b/Calculator/AppDelegate.swift @@ -35,20 +35,24 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusBarItem?.button?.image = NSImage(systemSymbolName: "function", accessibilityDescription: "calculator") statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:)) + // enables global shortcut hotKey.keyDownHandler = { self.togglePopover(self) } } + @objc func showPopover(_ sender: AnyObject?) { if let button = statusBarItem?.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) // !!! - displays the popover window with an offset in x in macOS BigSur. } } + @objc func closePopover(_ sender: AnyObject?) { popover.performClose(sender) } + @objc func togglePopover(_ sender: AnyObject?) { if popover.isShown { closePopover(sender) diff --git a/Calculator/CalculatorView.swift b/Calculator/CalculatorView.swift index 00f164e..bff801b 100644 --- a/Calculator/CalculatorView.swift +++ b/Calculator/CalculatorView.swift @@ -57,9 +57,9 @@ struct CalculatorView: View { }).rotationEffect(Angle(degrees: 180)).scaleEffect(x: -1.0, y: 1.0, anchor: .center) - MacEditorTextView(text: $expression, placeholderText: "Calculate", + CustomMacTextView(placeholderText: "Calculate", text: $expression, // Run when submmitting the text (hitting return) - onCommit: { + onSubmit: { do { solution = try evaluateExpression(expression) diff --git a/Calculator/CustomMacTextView.swift b/Calculator/CustomMacTextView.swift new file mode 100644 index 0000000..6056c48 --- /dev/null +++ b/Calculator/CustomMacTextView.swift @@ -0,0 +1,119 @@ +// +// CustomMacTextView.swift +// +// Foundation by Marc Maset - 2021 +// Changes inspired from MacEditorTextView by Thiago Holanda +// +// Modified by Anthony Ingle - 2022 +// + +import SwiftUI + +struct CustomMacTextView: NSViewRepresentable { + + var placeholderText: String? + @Binding var text: String + var font: NSFont = .systemFont(ofSize: 14, weight: .regular) + + var onSubmit : () -> Void = {} + var onTextChange : (String) -> Void = { _ in } + var onEditingChanged: () -> Void = {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeNSView(context: Context) -> NSScrollView { + let theTextView = PlaceholderNSTextView.scrollableTextView() + let textView = (theTextView.documentView as! PlaceholderNSTextView) + textView.delegate = context.coordinator + textView.string = text + textView.drawsBackground = false + textView.font = font + textView.placeholderText = placeholderText + theTextView.hasVerticalScroller = false + + return theTextView + } + + func updateNSView(_ view: NSScrollView, context: Context) { + guard let textView = view.documentView as? NSTextView else { + return + } + + textView.string = text + } + +} + +extension CustomMacTextView { + + class Coordinator: NSObject, NSTextViewDelegate { + + var parent: CustomMacTextView + var affectedCharRange: NSRange? + + init(_ parent: CustomMacTextView) { + 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 + } + + // Update text + self.parent.text = textView.string + 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.onSubmit() + } + + + func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool { + return true + } + + // handles commands + func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if (commandSelector == #selector(NSResponder.insertNewline(_:))) { + // Do something against ENTER key + self.parent.onSubmit() + return true + } + + // return true if the action was handled; otherwise false + return false + } + + } +} + +// for setting a proper placeholder text on an NSTextView +fileprivate class PlaceholderNSTextView: NSTextView { + @objc private var placeholderAttributedString: NSAttributedString? + var placeholderText: String? { + didSet { + var attributes = [NSAttributedString.Key: AnyObject]() + attributes[.font] = font + attributes[.foregroundColor] = NSColor.gray + let captionAttributedString = NSAttributedString(string: placeholderText ?? "", attributes: attributes) + placeholderAttributedString = captionAttributedString + } + } +} diff --git a/Calculator/MacEditorTextView.swift b/Calculator/MacEditorTextView.swift deleted file mode 100644 index 9b6ce2c..0000000 --- a/Calculator/MacEditorTextView.swift +++ /dev/null @@ -1,268 +0,0 @@ - -/** - * MacEditorTextView - * Copyright (c) Thiago Holanda 2020-2021 - * https://twitter.com/tholanda - * - * MIT license - * - * Edited by Anthony Ingle - */ - -import Combine -import SwiftUI - -struct MacEditorTextView: NSViewRepresentable { - @Binding var text: String - var placeholderText: String? - var isEditable: Bool = true - var font: NSFont? = .systemFont(ofSize: 14, weight: .regular) - - 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, - font: font, - placeholderText: placeholderText - ) - textView.delegate = context.coordinator - - return textView - } - - func updateNSView(_ view: CustomTextView, context: Context) { - view.text = text - view.selectedRanges = context.coordinator.selectedRanges - } -} - -// MARK: - Preview - -#if DEBUG - -struct MacEditorTextView_Previews: PreviewProvider { - static var previews: some View { - Group { - MacEditorTextView( - text: .constant("{ \n planets { \n name \n }\n}"), - isEditable: true, - font: .userFixedPitchFont(ofSize: 14) - ) - .environment(\.colorScheme, .dark) - .previewDisplayName("Dark Mode") - - MacEditorTextView( - text: .constant("{ \n planets { \n name \n }\n}"), - isEditable: false - ) - .environment(\.colorScheme, .light) - .previewDisplayName("Light Mode") - } - } -} - -#endif - -// MARK: - Coordinator - -extension MacEditorTextView { - - class Coordinator: NSObject, NSTextViewDelegate { - var parent: MacEditorTextView - var selectedRanges: [NSValue] = [] - - 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() - } - - // handles commands - func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - if (commandSelector == #selector(NSResponder.insertNewline(_:))) { - // Do something against ENTER key - self.parent.onCommit() - return true - } - // keeping commented for future use - // } else if (commandSelector == #selector(NSResponder.deleteForward(_:))) { - // // Do something against DELETE key - // return true - // } else if (commandSelector == #selector(NSResponder.deleteBackward(_:))) { - // // Do something against BACKSPACE key - // return true - // } else if (commandSelector == #selector(NSResponder.insertTab(_:))) { - // // Do something against TAB key - // return true - // } else if (commandSelector == #selector(NSResponder.cancelOperation(_:))) { - // // Do something against ESCAPE key - // return true - // } - - // return true if the action was handled; otherwise false - return false - } - - } -} - -// MARK: - CustomTextView - -final class CustomTextView: NSView { - private var isEditable: Bool - private var font: NSFont? - private var placeholderText: String? - - 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 = false - 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 = PlaceholderNSTextView(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.allowsUndo = true - textView.placeholderText = self.placeholderText - - return textView - }() - - // MARK: - Init - init(text: String, isEditable: Bool, font: NSFont?, placeholderText: String?) { - self.font = font - self.isEditable = isEditable - self.text = text - self.placeholderText = placeholderText - - 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() - } - - 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 - } -} - -// for setting a proper placeholder text on an NSTextView -class PlaceholderNSTextView: NSTextView { - @objc private var placeholderAttributedString: NSAttributedString? - var placeholderText: String? { - didSet { - var attributes = [NSAttributedString.Key: AnyObject]() - attributes[.font] = font - attributes[.foregroundColor] = NSColor.gray - let captionAttributedString = NSAttributedString(string: placeholderText ?? "", attributes: attributes) - placeholderAttributedString = captionAttributedString - } - } -}