【问题标题】:NSLayoutManager hides new line characters no matter what I doNSLayoutManager 无论我做什么都会隐藏换行符
【发布时间】:2016-09-17 10:25:56
【问题描述】:

我试图在我的 NSTextView 子类中显示不可见字符,例如换行符。像覆盖 NSLayoutManager 的 drawGlyph 方法这样的常用方法是一个坏主意,因为它太慢并且不能在多页布局中正常工作。

我想要做的是重写 NSLayoutManager 的 setGlyph 方法,以便它将不可见的“\n”字形替换为“¶”字形,并将“”替换为“∙”。

它适用于 " " 空格字形,但对换行符没有影响。

public override func setGlyphs(_ glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSGlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: Font, forGlyphRange glyphRange: NSRange) {
    var substring = (self.currentTextStorage.string as NSString).substring(with: glyphRange)

    // replace invisible characters with visible
    if PreferencesManager.shared.shouldShowInvisibles == true {
        substring = substring.replacingOccurrences(of: " ", with: "\u{00B7}")
        substring = substring.replacingOccurrences(of: "\n", with: "u{00B6}")
    }

    // create a CFString
    let stringRef = substring as CFString
    let count = CFStringGetLength(stringRef)

    // convert processed string to the C-pointer
    let cfRange = CFRangeMake(0, count)
    let fontRef = CTFontCreateWithName(aFont.fontName as CFString?, aFont.pointSize, nil)
    let characters = UnsafeMutablePointer<UniChar>.allocate(capacity: MemoryLayout<UniChar>.size * count)
    CFStringGetCharacters(stringRef, cfRange, characters)

    // get glyphs for the pointer of characters
    let glyphsRef = UnsafeMutablePointer<CGGlyph>.allocate(capacity: MemoryLayout<CGGlyph>.size * count)
    CTFontGetGlyphsForCharacters(fontRef, characters, glyphsRef, count)

    // set those glyphs
    super.setGlyphs(glyphsRef, properties:props, characterIndexes: charIndexes, font: aFont, forGlyphRange: glyphRange)
}

然后我想出了一个主意:看起来 NSTypesetter 将新行字符范围标记为根本不应该处理的字符范围。所以我继承了 NSTypesetter 并覆盖了一个方法:

override func setNotShownAttribute(_ flag: Bool, forGlyphRange glyphRange: NSRange) {
    let theFlag = PreferencesManager.shared.shouldShowInvisibles == true ? false : true
    super.setNotShownAttribute(theFlag, forGlyphRange: glyphRange)
}

但它不起作用。 NSLayoutManager 仍然不会为换行符生成一个字形,无论我创建什么字形。

我做错了什么?

【问题讨论】:

    标签: swift cocoa cocoa-touch appkit nslayoutmanager


    【解决方案1】:

    据我所知,NSTypesetter 的 setNotShownAttribute: 类的默认实现不会更改其字形存储中已生成的字形。因此,调用 super 不会产生任何效果。我只需要在调用 super 之前手动替换字形。

    因此,显示不可见字符的最有效实现(缩放视图时会看到差异)是这样的:

    这种方法的局限性:如果你的应用必须在文本视图中有多种字体,那么这种方法可能不是一个好主意,因为那些显示的不可见字符的字体会有所不同好。这不是您可能想要实现的目标。

    1. 子类 NSLayoutManager 并覆盖 setGlyphs 以显示空格字符:

      public override func setGlyphs(_ glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSGlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: Font, forGlyphRange glyphRange: NSRange) {
          var substring = (self.currentTextStorage.string as NSString).substring(with: glyphRange)
      
          // replace invisible characters with visible
          if PreferencesManager.shared.shouldShowInvisibles == true {
              substring = substring.replacingOccurrences(of: " ", with: "\u{00B7}")
          }
      
          // create a CFString
          let stringRef = substring as CFString
          let count = CFStringGetLength(stringRef)
      
          // convert processed string to the C-pointer
          let cfRange = CFRangeMake(0, count)
          let fontRef = CTFontCreateWithName(aFont.fontName as CFString?, aFont.pointSize, nil)
          let characters = UnsafeMutablePointer<UniChar>.allocate(capacity: MemoryLayout<UniChar>.size * count)
          CFStringGetCharacters(stringRef, cfRange, characters)
      
          // get glyphs for the pointer of characters
          let glyphsRef = UnsafeMutablePointer<CGGlyph>.allocate(capacity: MemoryLayout<CGGlyph>.size * count)
          CTFontGetGlyphsForCharacters(fontRef, characters, glyphsRef, count)
      
          // set those glyphs
          super.setGlyphs(glyphsRef, properties:props, characterIndexes: charIndexes, font: aFont, forGlyphRange: glyphRange)
      }
      
    2. 子类 NSATSTypesetter 并将其分配给您的 NSLayoutManager 子类。子类将显示换行符,并确保每个不可见的字符都用不同的颜色绘制:

      class CustomTypesetter: NSATSTypesetter {
      
          override func setNotShownAttribute(_ flag: Bool, forGlyphRange glyphRange: NSRange) {
              var theFlag = flag
      
              if PreferencesManager.shared.shouldShowInvisibles == true   {
                  theFlag = false
      
                  // add new line glyphs into the glyph storage
                  var newLineGlyph = yourFont.glyph(withName: "paragraph")
                  self.substituteGlyphs(in: glyphRange, withGlyphs: &newLineGlyph)
      
                  // draw new line char with different color
                  self.layoutManager?.addTemporaryAttribute(NSForegroundColorAttributeName, value: NSColor.invisibleTextColor, forCharacterRange: glyphRange)
              }
      
              super.setNotShownAttribute(theFlag, forGlyphRange: glyphRange)
          }
      
          /// Currently hadn't found any faster way to draw space glyphs with different color
          override func setParagraphGlyphRange(_ paragraphRange: NSRange, separatorGlyphRange paragraphSeparatorRange: NSRange) {
              super.setParagraphGlyphRange(paragraphRange, separatorGlyphRange: paragraphSeparatorRange)
      
              guard PreferencesManager.shared.shouldShowInvisibles == true else { return }
      
              if let substring = (self.layoutManager?.textStorage?.string as NSString?)?.substring(with: paragraphRange) {
                  let expression = try? NSRegularExpression.init(pattern: "\\s", options: NSRegularExpression.Options.useUnicodeWordBoundaries)
                  let sunstringRange = NSRange(location: 0, length: substring.characters.count)
      
                  if let matches = expression?.matches(in: substring, options: NSRegularExpression.MatchingOptions.withoutAnchoringBounds, range: sunstringRange) {
                      for match in matches {
                          let globalSubRange = NSRange(location: paragraphRange.location + match.range.location, length: 1)
                          self.layoutManager?.addTemporaryAttribute(NSForegroundColorAttributeName, value: Color.invisibleText, forCharacterRange: globalSubRange)
                      }
                  }
              }
          }
      }
      
    3. 要显示/隐藏不可见字符,只需调用:

      let storageRange = NSRange(location: 0, length: currentTextStorage.length)
      layoutManager.invalidateGlyphs(forCharacterRange: storageRange, changeInLength: 0, actualCharacterRange: nil)
      layoutManager.ensureGlyphs(forGlyphRange: storageRange)
      

    【讨论】:

    • 似乎这段代码在 Swift3 中,我们如何在 Swift 2.3 中转换它?我注意到“MemoryLayout”在 Swift 2.3 中不可用。
    【解决方案2】:

    这可能已经超出了这个问题的用处,但我是通过谷歌到达这里的。

    至少在 macOS 10.14 上进行测试时,-[NSLayoutManager setNotShownAttribute:forGlyphAtIndex:] 能够在生成该索引的字形后更改其值。关键是它将无条件地setNotShownAttribute:YES 用于布局行末尾的换行符。完成后你会得到-[NSLayoutManagerDelegate layoutManager:shouldUseAction:forControlCharacterAtIndex:],你可以在那里重置它:

    - (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)characterIndex {
      if (layoutManager.showsInvisibleCharacters && (action & NSControlCharacterActionLineBreak)) {
        [layoutManager setNotShownAttribute:NO forGlyphAtIndex:[layoutManager glyphIndexForCharacterAtIndex:characterIndex]];
      }
    
      return action;
    }
    
    func layoutManager(_ layoutManager: NSLayoutManager, shouldUse action: NSLayoutManager.ControlCharacterAction, forControlCharacterAt characterIndex: Int) -> NSLayoutManager.ControlCharacterAction {
      if layoutManager.showsInvisibleCharacters, action.contains(.lineBreak) {
        let glyphIndex = layoutManager.glyphIndexForCharacter(at: characterIndex)
        layoutManager.setNotShownAttribute(false, forGlyphAt: glyphIndex)
      }
    
      return action
    }
    

    您会注意到我在示例中使用了showsInvisibleCharacters,它实际上适用于内置方法,即使它没有该字符的映射,产生“我不知道”字形:

    使用-[NSLayoutManagerDelegate layoutManager:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:,您可以让它完美运行:

    【讨论】:

    • 好时机!我正在尝试重写我的旧实现以获得更好的性能。我实现了你的 shouldUseAction... 委托方法。我 setShows... 控制和隐形字符。根据我使用的字体,我会得到与您的第一个类似的图像。我正在努力寻找一个好的资源来了解更多关于 layoutManager:shouldGenerateGlyphs:... 方法来使用第二张图片中显示的字形。任何指针?谢谢!!!
    • 经过数小时的“暴力破解”——我的工作解决方案包括排版器 setNotShown...、layoutManager:shouldGenerateGlyphs...(将新字形换成空格和换行符(选项卡不起作用在这里?),并使用 drawBackgroundForGlyphRange 提供可见选项卡(受stackoverflow.com/questions/36666502/… 启发)。对选项卡使用单独的方法而不是空格/换行符似乎很奇怪。我这样做是对的,还是有更好的方法?(要么方式,这给了我比我的旧方法更快、更准确的结果!)
    猜你喜欢
    • 2015-08-25
    • 1970-01-01
    • 1970-01-01
    • 2011-04-26
    • 1970-01-01
    • 2022-11-14
    • 2012-03-11
    • 2017-11-21
    • 2013-12-25
    相关资源
    最近更新 更多