【问题标题】:Replacing NSAttributedString in NSTextStorage Moves NSTextView Cursor替换 NSTextStorage 中的 NSAttributedString 移动 NSTextView 光标
【发布时间】:2019-08-13 18:58:48
【问题描述】:

我关注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 类,将 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上的相同代码(来自上述教程)没有这个问题。

有什么想法吗?

【问题讨论】:

  • 保存文本视图的选定范围。完成属性化后,将保存的文本范围放回去。

标签: swift nsattributedstring nstextview nstextstorage


【解决方案1】:

我想我(希望)在阅读这篇文章后想通了: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(),而上述文章的重点是 您不能在TextStorageprocessEditing() 函数中应用样式 否则世界会爆炸(并且光标会移动到最后)。

我希望这对其他人有帮助!

【讨论】:

  • 我想补充一点,不仅世界会爆炸 :) 而且 NSLayoutManager 可能会得到原始用户编辑范围(文本和属性发生变化)和语法高亮范围的联合(只有属性改变)。光标在.editedCharacters 上移动,受影响范围的并集将这两个变化合并为[.editedCharacters, .editedAttributes] 的单个editMask
  • 确实如此。有完全相同的问题。我用这个作为样本。 github.com/mattrighetti/SimpleCodeEditor 并为 Mac 重新编写了它,并且遇到了完全相同的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多