无论我做什么,NSLayoutManager 都会隐藏换行符
NSLayoutManager hides new line characters no matter what I do
我试图在我的 NSTextView 子类中显示不可见字符,例如换行符。覆盖 NSLayoutManager 的 drawGlyph 方法之类的常用方法是一个坏主意,因为它太慢并且无法在多页布局中正常工作。
我想做的是重写 NSLayoutManager 的 setGlyph 方法,这样它将不可见的“\n”字形替换为“¶”字形,将“”替换为“∙”。
它适用于“”space 字形,但对换行符没有影响。
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 仍然不会为换行符生成字形。
我做错了什么?
据我所知,class 的 NSTypesetter 的 setNotShownAttribute: 的默认实现不会更改其字形存储中已生成的字形。因此,调用 super 不会产生任何效果。我只需要在调用 super 之前手动替换字形。
因此,显示不可见字符的最有效实现(缩放视图时您会看到差异)是这样的:
这种方法的局限性:如果您的应用必须在文本视图中使用多种字体,那么这种方法可能不是一个好主意,因为那些显示的字体是不可见的字符也会不同。这不是您想要实现的目标。
Subclass NSLayoutManager 并覆盖 setGlyphs 以显示 space 个字符:
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)
}
Subclass NSATSTypesetter 并将其分配给您的 NSLayoutManager 子类。 subclass 将显示新行字符,并确保每个不可见的字符都将以不同的颜色绘制:
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)
}
}
}
}
}
要show/hide个不可见字符只需调用:
let storageRange = NSRange(location: 0, length: currentTextStorage.length)
layoutManager.invalidateGlyphs(forCharacterRange: storageRange, changeInLength: 0, actualCharacterRange: nil)
layoutManager.ensureGlyphs(forGlyphRange: storageRange)
这可能已经超出了这个问题的用处,但我是通过 Google.
到达这里的
至少在 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
,它实际上适用于内置方法,即使它没有针对该字符的映射,生成 "I don't know" 字形:
使用 -[NSLayoutManagerDelegate layoutManager:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:
,您可以让它完美运行:
我试图在我的 NSTextView 子类中显示不可见字符,例如换行符。覆盖 NSLayoutManager 的 drawGlyph 方法之类的常用方法是一个坏主意,因为它太慢并且无法在多页布局中正常工作。
我想做的是重写 NSLayoutManager 的 setGlyph 方法,这样它将不可见的“\n”字形替换为“¶”字形,将“”替换为“∙”。
它适用于“”space 字形,但对换行符没有影响。
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 仍然不会为换行符生成字形。
我做错了什么?
据我所知,class 的 NSTypesetter 的 setNotShownAttribute: 的默认实现不会更改其字形存储中已生成的字形。因此,调用 super 不会产生任何效果。我只需要在调用 super 之前手动替换字形。
因此,显示不可见字符的最有效实现(缩放视图时您会看到差异)是这样的:
这种方法的局限性:如果您的应用必须在文本视图中使用多种字体,那么这种方法可能不是一个好主意,因为那些显示的字体是不可见的字符也会不同。这不是您想要实现的目标。
Subclass NSLayoutManager 并覆盖 setGlyphs 以显示 space 个字符:
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) }
Subclass NSATSTypesetter 并将其分配给您的 NSLayoutManager 子类。 subclass 将显示新行字符,并确保每个不可见的字符都将以不同的颜色绘制:
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) } } } } }
要show/hide个不可见字符只需调用:
let storageRange = NSRange(location: 0, length: currentTextStorage.length) layoutManager.invalidateGlyphs(forCharacterRange: storageRange, changeInLength: 0, actualCharacterRange: nil) layoutManager.ensureGlyphs(forGlyphRange: storageRange)
这可能已经超出了这个问题的用处,但我是通过 Google.
到达这里的至少在 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
,它实际上适用于内置方法,即使它没有针对该字符的映射,生成 "I don't know" 字形:
使用 -[NSLayoutManagerDelegate layoutManager:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:
,您可以让它完美运行: