Skip to content

Commit

Permalink
Add Find feature. Setup NSTextFinderClient.
Browse files Browse the repository at this point in the history
  • Loading branch information
krzyzanowskim committed Apr 16, 2022
1 parent 6a12e46 commit da5e436
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 58 deletions.
22 changes: 22 additions & 0 deletions Sources/STTextView/NSTextContentManager+Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Created by Marcin Krzyzanowski
// https://github.com/krzyzanowskim/STTextView/blob/main/LICENSE.md

import Cocoa

extension NSTextContentManager {

var documentString: String {
var result: String = ""
result.reserveCapacity(1024 * 4)

enumerateTextElements(from: nil, options: []) { textElement in
if let textParagraph = textElement as? NSTextParagraph {
result += textParagraph.attributedString.string
}

return true
}
return result
}

}
1 change: 0 additions & 1 deletion Sources/STTextView/NSTextLayoutManager+Helpers.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Created by Marcin Krzyzanowski
// https://github.com/krzyzanowskim/STTextView/blob/main/LICENSE.md


import Cocoa

extension NSTextLayoutManager {
Expand Down
124 changes: 124 additions & 0 deletions Sources/STTextView/STTextFinderClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Created by Marcin Krzyzanowski
// https://github.com/krzyzanowskim/STTextView/blob/main/LICENSE.md

import Cocoa

final class STTextFinderClient: NSObject, NSTextFinderClient {

weak var textView: STTextView?

private var textContentManager: NSTextContentManager? {
textView?.textContentStorage
}

var string: String {
textView?.string ?? ""
}

func stringLength() -> Int {
string.count
}

var isSelectable: Bool {
textView?.isSelectable ?? false
}

public var allowsMultipleSelection: Bool {
false
}

public var firstSelectedRange: NSRange {
guard let firstTextSelectionRange = textView?.textLayoutManager.textSelections.first?.textRanges.first,
let textContentManager = textContentManager else {
return NSRange()
}

return NSRange(firstTextSelectionRange, in: textContentManager)
}

public var selectedRanges: [NSValue] {
set {
guard let textContentManager = textContentManager,
let textLayoutManager = textView?.textLayoutManager as? STTextLayoutManager else {
assertionFailure()
return
}

let textRanges = newValue.map(\.rangeValue).compactMap {
NSTextRange($0, in: textContentManager)
}

textLayoutManager.textSelections = [NSTextSelection(textRanges, affinity: .downstream, granularity: .character)]
textView?.updateSelectionHighlights()
}

get {
guard let textContentManager = textContentManager,
let textLayoutManager = textView?.textLayoutManager,
!textLayoutManager.textSelections.isEmpty
else {
return []
}

return textLayoutManager.textSelections.flatMap(\.textRanges).compactMap({ NSRange($0, in: textContentManager) }).map({ NSValue(range: $0) })
}
}

var isEditable: Bool {
textView?.isEditable ?? false
}

func scrollRangeToVisible(_ range: NSRange) {
guard let textContentManager = textContentManager,
let textRange = NSTextRange(range, in: textContentManager)
else {
return
}
textView?.scrollToSelection(NSTextSelection(range: textRange, affinity: .downstream, granularity: .character))
}

var visibleCharacterRanges: [NSValue] {
guard let viewportTextRange = textView?.textLayoutManager.textViewportLayoutController.viewportRange,
let textContentManager = textContentManager else {
return []
}

return [NSRange(viewportTextRange, in: textContentManager)].map({ NSValue(range: $0) })
}

func rects(forCharacterRange range: NSRange) -> [NSValue]? {
guard let textContentManager = textContentManager,
let textRange = NSTextRange(range, in: textContentManager)
else {
return nil
}

var rangeRects: [CGRect] = []
textView?.textLayoutManager.enumerateTextSegments(in: textRange, type: .standard, options: .rangeNotRequired, using: { _, rect, _, _ in
rangeRects.append(rect.pixelAligned)
return true
})

return rangeRects.map({ NSValue(rect: $0) })
}

func contentView(at index: Int, effectiveCharacterRange outRange: NSRangePointer) -> NSView {
outRange.pointee = NSRange(location: 0, length: stringLength())
return textView!
}

func drawCharacters(in range: NSRange, forContentView view: NSView) {
guard let textView = view as? STTextView,
let textRange = NSTextRange(range, in: textView.textContentStorage),
let context = NSGraphicsContext.current?.cgContext
else {
assertionFailure()
return
}

if let layoutFragment = textView.textLayoutManager.textLayoutFragment(for: textRange.location) {
layoutFragment.draw(at: layoutFragment.layoutFragmentFrame.origin, in: context)
}
}

}
9 changes: 3 additions & 6 deletions Sources/STTextView/STTextLayoutManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@

import Cocoa

final class STTextLayoutManager: NSTextLayoutManager {
public final class STTextLayoutManager: NSTextLayoutManager {

static let didChangeSelectionNotification = STTextView.didChangeSelectionNotification

override var textSelections: [NSTextSelection] {
public override var textSelections: [NSTextSelection] {
didSet {
let notification = Notification(name: STTextLayoutManager.didChangeSelectionNotification, object: self, userInfo: nil)
let notification = Notification(name: STTextView.didChangeSelectionNotification, object: self, userInfo: nil)
NotificationCenter.default.post(notification)
}
}

}
4 changes: 4 additions & 0 deletions Sources/STTextView/STTextView+Delete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ extension STTextView {
var didChange = false
textContentStorage.performEditingTransaction {
for textRange in textRanges where shouldChangeText(in: textRange, replacementString: nil) {
if didChange == false {
willChangeText()
}

didChange = true
let nsrange = NSRange(textRange, in: textContentStorage)
textContentStorage.textStorage?.deleteCharacters(in: nsrange)
Expand Down
22 changes: 22 additions & 0 deletions Sources/STTextView/STTextView+Find.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Created by Marcin Krzyzanowski
// https://github.com/krzyzanowskim/STTextView/blob/main/LICENSE.md

import Cocoa

extension STTextView {

@objc func performFindPanelAction(_ sender: Any?) {
performTextFinderAction(sender)
}

@objc open override func performTextFinderAction(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem else {
assertionFailure("Unexpected caller")
return
}


textFinder.performAction(NSTextFinder.Action(rawValue: menuItem.tag)!)
}

}
42 changes: 42 additions & 0 deletions Sources/STTextView/STTextView+NSUserInterfaceValidations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Created by Marcin Krzyzanowski
// https://github.com/krzyzanowskim/STTextView/blob/main/LICENSE.md

import Cocoa

extension STTextView: NSMenuItemValidation {

public func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
validateUserInterfaceItem(menuItem)
}

}

extension STTextView: NSUserInterfaceValidations {

public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
switch item.action {
case #selector(undo(_:)):
let result = allowsUndo ? undoManager?.canUndo ?? false : false

// NSWindow does that like this, here (as debugged)
if let undoManager = undoManager {
(item as? NSMenuItem)?.title = undoManager.undoMenuItemTitle
}

return result
case #selector(redo(_:)):
let result = allowsUndo ? undoManager?.canRedo ?? false : false

// NSWindow does that like this, here (as debugged)
if let undoManager = undoManager {
(item as? NSMenuItem)?.title = undoManager.redoMenuItemTitle
}
return result
case #selector(performFindPanelAction(_:)), #selector(performTextFinderAction(_:)):
return textFinder.validateAction(NSTextFinder.Action(rawValue: item.tag)!)
default:
return true
}
}

}
18 changes: 8 additions & 10 deletions Sources/STTextView/STTextView+TextInputClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ extension STTextView: NSTextInputClient {

open func insertText(_ string: Any, replacementRange: NSRange) {
guard isEditable else { return }
var didChange = false

textContentStorage.performEditingTransaction {
switch string {
case is String:
Expand All @@ -94,14 +92,16 @@ extension STTextView: NSTextInputClient {
}
if let textRange = NSTextRange(replacementRange, in: textContentStorage) {
if shouldChangeText(in: textRange, replacementString: string) {
willChangeText()
replaceCharacters(in: textRange, with: string)
didChange = true
didChangeText()
}
} else if !textLayoutManager.textSelections.isEmpty {
for textRange in textLayoutManager.textSelections.flatMap(\.textRanges) {
if shouldChangeText(in: textRange, replacementString: string) {
willChangeText()
replaceCharacters(in: textRange, with: string)
didChange = true
didChangeText()
}
}
}
Expand All @@ -111,14 +111,16 @@ extension STTextView: NSTextInputClient {
}
if let textRange = NSTextRange(replacementRange, in: textContentStorage) {
if shouldChangeText(in: textRange, replacementString: string.string) {
willChangeText()
replaceCharacters(in: textRange, with: string)
didChange = true
didChangeText()
}
} else if !textLayoutManager.textSelections.isEmpty {
for textRange in textLayoutManager.textSelections.flatMap(\.textRanges) {
if shouldChangeText(in: textRange, replacementString: string.string) {
willChangeText()
replaceCharacters(in: textRange, with: string)
didChange = true
didChangeText()
}
}
}
Expand All @@ -127,10 +129,6 @@ extension STTextView: NSTextInputClient {
}

}

if didChange {
didChangeText()
}
}


Expand Down
35 changes: 0 additions & 35 deletions Sources/STTextView/STTextView+Undo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,3 @@ extension STTextView {
}

}

extension STTextView: NSMenuItemValidation {

public func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
validateUserInterfaceItem(menuItem)
}

}

extension STTextView: NSUserInterfaceValidations {

public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(undo(_:)) {
let result = allowsUndo ? undoManager?.canUndo ?? false : false

// NSWindow does that like this, here (as debugged)
if let undoManager = undoManager {
(item as? NSMenuItem)?.title = undoManager.undoMenuItemTitle
}

return result
} else if item.action == #selector(redo(_:)) {
let result = allowsUndo ? undoManager?.canRedo ?? false : false

// NSWindow does that like this, here (as debugged)
if let undoManager = undoManager {
(item as? NSMenuItem)?.title = undoManager.redoMenuItemTitle
}
return result
}

return true
}

}
Loading

0 comments on commit da5e436

Please sign in to comment.