替换 NSTextStorage 中的 NSAttributedString 移动 NSTextView 光标
Replacing NSAttributedString in NSTextStorage Moves NSTextView Cursor
我关注 this tutorial 并创建了它的 Mac 版本。它工作得很好,除了有一个我无法弄清楚的错误。如果您尝试编辑字符串中间的任何内容,光标会跳到字符串的末尾。像这样:
这是一个sample project,或者你可以只创建一个新的macOS项目并将其放入默认ViewController.swift:
import Cocoa
class ViewController: NSViewController, NSTextViewDelegate {
var textView: NSTextView!
var textStorage: FancyTextStorage!
override func viewDidLoad() {
super.viewDidLoad()
createTextView()
}
func createTextView() {
// 1
let attrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13)]
let attrString = NSAttributedString(string: "This is a *cool* sample.", attributes: attrs)
textStorage = FancyTextStorage()
textStorage.append(attrString)
let newTextViewRect = view.bounds
// 2
let layoutManager = NSLayoutManager()
// 3
let containerSize = CGSize(width: newTextViewRect.width, height: .greatestFiniteMagnitude)
let container = NSTextContainer(size: containerSize)
container.widthTracksTextView = true
layoutManager.addTextContainer(container)
textStorage.addLayoutManager(layoutManager)
// 4
textView = NSTextView(frame: newTextViewRect, textContainer: container)
textView.delegate = self
view.addSubview(textView)
// 5
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
然后创建一个 FancyTextStorage class 子classes NSTextStorage
与此:
class FancyTextStorage: NSTextStorage{
let backingStore = NSMutableAttributedString()
private var replacements: [String: [NSAttributedString.Key: Any]] = [:]
override var string: String {
return backingStore.string
}
override init() {
super.init()
createHighlightPatterns()
}
func createHighlightPatterns() {
let boldAttributes = [NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 13)]
replacements = ["(\*\w+(\s\w+)*\*)": boldAttributes]
}
func applyStylesToRange(searchRange: NSRange) {
let normalAttrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13, weight: .regular), NSAttributedString.Key.foregroundColor: NSColor.init(calibratedRed: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)]
addAttributes(normalAttrs, range: searchRange)
// iterate over each replacement
for (pattern, attributes) in replacements {
do {
let regex = try NSRegularExpression(pattern: pattern)
regex.enumerateMatches(in: backingStore.string, range: searchRange) {
match, flags, stop in
// apply the style
if let matchRange = match?.range(at: 1) {
print("Matched pattern: \(pattern)")
addAttributes(attributes, range: matchRange)
// reset the style to the original
let maxRange = matchRange.location + matchRange.length
if maxRange + 1 < length {
addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
}
}
}
}
catch {
print("An error occurred attempting to locate pattern: " +
"\(error.localizedDescription)")
}
}
}
func performReplacementsForRange(changedRange: NSRange) {
var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(changedRange.location, 0)))
extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(NSMaxRange(changedRange), 0)))
beginEditing()
applyStylesToRange(searchRange: extendedRange)
endEditing()
}
override func processEditing() {
performReplacementsForRange(changedRange: editedRange)
super.processEditing()
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
print("replaceCharactersInRange:\(range) withString:\(str)")
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
//print("setAttributes:\(String(describing: attrs)) range:\(range)")
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
}
}
似乎在重写字符串时,它不会保留光标位置,但是 iOS 上的相同代码(来自上述教程)没有这个问题。
有什么想法吗?
我想我(希望)在阅读这篇文章后明白了:https://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/
在我的 ViewController.swift 中,我添加了 textDidChange
委托方法和用于更新样式的可重用函数:
func textDidChange(_ notification: Notification) {
updateStyles()
}
func updateStyles(){
guard let fancyTextStorage = textView.textStorage as? FancyTextStorage else { return }
fancyTextStorage.beginEditing()
fancyTextStorage.applyStylesToRange(searchRange: fancyTextStorage.extendedRange)
fancyTextStorage.endEditing()
}
然后在 FancyTextStorage 中,我必须从 processEditing()
中 删除 performReplacementsForRange
因为它调用 applyStylesToRange()
而上述文章的要点是 你不能在 TextStorage
的 processEditing()
函数中应用样式 否则世界将会爆炸(并且光标会移动到末尾)。
我希望这对其他人有帮助!
我关注 this tutorial 并创建了它的 Mac 版本。它工作得很好,除了有一个我无法弄清楚的错误。如果您尝试编辑字符串中间的任何内容,光标会跳到字符串的末尾。像这样:
这是一个sample project,或者你可以只创建一个新的macOS项目并将其放入默认ViewController.swift:
import Cocoa
class ViewController: NSViewController, NSTextViewDelegate {
var textView: NSTextView!
var textStorage: FancyTextStorage!
override func viewDidLoad() {
super.viewDidLoad()
createTextView()
}
func createTextView() {
// 1
let attrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13)]
let attrString = NSAttributedString(string: "This is a *cool* sample.", attributes: attrs)
textStorage = FancyTextStorage()
textStorage.append(attrString)
let newTextViewRect = view.bounds
// 2
let layoutManager = NSLayoutManager()
// 3
let containerSize = CGSize(width: newTextViewRect.width, height: .greatestFiniteMagnitude)
let container = NSTextContainer(size: containerSize)
container.widthTracksTextView = true
layoutManager.addTextContainer(container)
textStorage.addLayoutManager(layoutManager)
// 4
textView = NSTextView(frame: newTextViewRect, textContainer: container)
textView.delegate = self
view.addSubview(textView)
// 5
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
然后创建一个 FancyTextStorage class 子classes NSTextStorage
与此:
class FancyTextStorage: NSTextStorage{
let backingStore = NSMutableAttributedString()
private var replacements: [String: [NSAttributedString.Key: Any]] = [:]
override var string: String {
return backingStore.string
}
override init() {
super.init()
createHighlightPatterns()
}
func createHighlightPatterns() {
let boldAttributes = [NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 13)]
replacements = ["(\*\w+(\s\w+)*\*)": boldAttributes]
}
func applyStylesToRange(searchRange: NSRange) {
let normalAttrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13, weight: .regular), NSAttributedString.Key.foregroundColor: NSColor.init(calibratedRed: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)]
addAttributes(normalAttrs, range: searchRange)
// iterate over each replacement
for (pattern, attributes) in replacements {
do {
let regex = try NSRegularExpression(pattern: pattern)
regex.enumerateMatches(in: backingStore.string, range: searchRange) {
match, flags, stop in
// apply the style
if let matchRange = match?.range(at: 1) {
print("Matched pattern: \(pattern)")
addAttributes(attributes, range: matchRange)
// reset the style to the original
let maxRange = matchRange.location + matchRange.length
if maxRange + 1 < length {
addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
}
}
}
}
catch {
print("An error occurred attempting to locate pattern: " +
"\(error.localizedDescription)")
}
}
}
func performReplacementsForRange(changedRange: NSRange) {
var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(changedRange.location, 0)))
extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(NSMaxRange(changedRange), 0)))
beginEditing()
applyStylesToRange(searchRange: extendedRange)
endEditing()
}
override func processEditing() {
performReplacementsForRange(changedRange: editedRange)
super.processEditing()
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
print("replaceCharactersInRange:\(range) withString:\(str)")
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
//print("setAttributes:\(String(describing: attrs)) range:\(range)")
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
}
}
似乎在重写字符串时,它不会保留光标位置,但是 iOS 上的相同代码(来自上述教程)没有这个问题。
有什么想法吗?
我想我(希望)在阅读这篇文章后明白了:https://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/
在我的 ViewController.swift 中,我添加了 textDidChange
委托方法和用于更新样式的可重用函数:
func textDidChange(_ notification: Notification) {
updateStyles()
}
func updateStyles(){
guard let fancyTextStorage = textView.textStorage as? FancyTextStorage else { return }
fancyTextStorage.beginEditing()
fancyTextStorage.applyStylesToRange(searchRange: fancyTextStorage.extendedRange)
fancyTextStorage.endEditing()
}
然后在 FancyTextStorage 中,我必须从 processEditing()
中 删除 performReplacementsForRange
因为它调用 applyStylesToRange()
而上述文章的要点是 你不能在 TextStorage
的 processEditing()
函数中应用样式 否则世界将会爆炸(并且光标会移动到末尾)。
我希望这对其他人有帮助!