NSLayoutManager hides newlines no matter what I do


维塔利(Vitaliy Vashchenko)

I'm trying to display invisible characters such as line breaks in a NSTextView subclass. Conventional methods like overriding NSLayoutManager's drawGlyph method are a bad idea, as it's too slow and doesn't work well with multi-page layouts.

What I want to do is to override the setGlyph method of NSLayoutManager so that the invisible "\n" glyphs are replaced with "¶" glyphs and "" with "∙".

It works on "" space glyphs, but not newlines.

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)
}

Then I came up with an idea: it looks like the NSTypesetter marks newline character ranges like those it shouldn't handle at all. So I subclass NSTypesetter and override a method:

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

but this doesn't work. No matter what glyphs I create, NSLayoutManager still doesn't generate glyphs for newlines.

What am I doing wrong?

维塔利(Vitaliy Vashchenko)

As far as I know, the default implementation of setNotShownAttribute: of the NSTypesetter of this class does not change the already generated glyphs in its glyph store. So calling super will have no effect. I just had to manually replace the glyphs before calling super.

So the most efficient implementation to display invisible characters (you will see the difference when you zoom in on the view) is:

Limitations of this approach: If your app has to have multiple fonts in the text view, this approach might not be a good idea, since those displayed invisible characters will also have different fonts. That's not what you might want to achieve.

  1. Subclass NSLayoutManager and override setGlyphs to display space characters:

    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. Subclass NSATSTypesetter and assign it to your NSLayoutManager subclass. Subclasses will display newlines and ensure that each invisible character will be drawn in a different color:

    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. To show/hide invisible characters, just call:

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

Related


NSLayoutManager hides newlines no matter what I do

维塔利(Vitaliy Vashchenko) I'm trying to display invisible characters such as line breaks in a NSTextView subclass. Conventional methods like overriding NSLayoutManager's drawGlyph method are a bad idea, as it's too slow and doesn't work well with multi-page layo

NSLayoutManager hides newlines no matter what I do

维塔利(Vitaliy Vashchenko) I'm trying to display invisible characters such as line breaks in a NSTextView subclass. Conventional methods like overriding NSLayoutManager's drawGlyph method are a bad idea, as it's too slow and doesn't work well with multi-page layo

NSLayoutManager hides newlines no matter what I do

维塔利(Vitaliy Vashchenko) I'm trying to display invisible characters such as line breaks in a NSTextView subclass. Conventional methods like overriding NSLayoutManager's drawGlyph method are a bad idea, as it's too slow and doesn't work well with multi-page layo

NSLayoutManager hides newlines no matter what I do

维塔利(Vitaliy Vashchenko) I'm trying to display invisible characters such as line breaks in a NSTextView subclass. Conventional methods like overriding NSLayoutManager's drawGlyph method are a bad idea, as it's too slow and doesn't work well with multi-page layo

UIButton selector is not called no matter what I do

Anthony Dev I know this question has been asked many times, but I didn't find a solution to my problem. I'm no stranger to iOs, this should be trivial, but it's driving me nuts :). Ok, I have class C which is a subclass of UIViewController. In it's view I have

jquery slideup not working no matter what i do

Nick Hessler I have a strange problem. First, I put the slideup() function into my website, but it didn't work. I've tried Alsorts and it doesn't work. I even tried copying and pasting the sample code into a new html file, that didn't even work. Any ideas? I c

jquery slideup not working no matter what i do

Nick Hessler I have a strange problem. First, I put the slideup() function into my website, but it didn't work. I've tried Alsorts and it doesn't work. I even tried copying and pasting the sample code into a new html file, that didn't even work. Any ideas? I c

UIButton selector is not called no matter what I do

Anthony Dev I know this question has been asked many times, but I didn't find a solution to my problem. I'm no stranger to iOs, this should be trivial, but it's driving me nuts :). Ok, I have class C which is a subclass of UIViewController. In it's view I have

jquery slideup not working no matter what i do

Nick Hessler I have a strange problem. First, I put the slideup() function into my website, but it didn't work. I've tried Alsorts and it doesn't work. I even tried copying and pasting the sample code into a new html file, that didn't even work. Any ideas? I c

Why do I get "403 Forbidden" no matter what I do?

flagg19 I'm trying to use boto to automate some operations on amazon ec2, but I can't run even the simplest example without getting: boto.exception.EC2ResponseError: EC2ResponseError: 403 Forbidden My code is: import boto.ec2 conn = boto.ec2.connect_to_regio

Why do I get "403 Forbidden" no matter what I do?

flagg19 I'm trying to use boto to automate some operations on amazon ec2, but I can't run even the simplest example without getting: boto.exception.EC2ResponseError: EC2ResponseError: 403 Forbidden My code is: import boto.ec2 conn = boto.ec2.connect_to_regio

Why do I get "403 Forbidden" no matter what I do?

flagg19 I'm trying to automate some operations on Amazon ec2 using boto, but I can't even run the simplest example: boto.exception.EC2ResponseError: EC2ResponseError: 403 Forbidden My code is: import boto.ec2 conn = boto.ec2.connect_to_region("us-west-2", aw

Why do I get "403 Forbidden" no matter what I do?

flagg19 I'm trying to automate some operations on Amazon ec2 using boto, but I can't even run the simplest example: boto.exception.EC2ResponseError: EC2ResponseError: 403 Forbidden My code is: import boto.ec2 conn = boto.ec2.connect_to_region("us-west-2", aw

No matter what I do, I run out of memory

Ole Henrik Skogstrom I am trying to use spark to filter a large dataframe. As a pandas dataframe, it's about 70GB in memory. I am able to load and filter this data using Pandas, but it is slow because I have to swap to disk etc. However, when I try to do this

I can zoom in on the parallax image no matter what I do

colorado powder This question seems to have been asked multiple times on different sites without a real "aha!" answer. I'm still pretty new and I know there are a hundred different ways to code this, but I'm hoping for a very simple solution that doesn't requi

I can zoom in on the parallax image no matter what I do

colorado powder This question seems to have been asked multiple times on different sites without a real "aha!" answer. I'm still pretty new and I know there are a hundred different ways to code this, but I'm hoping for a very simple solution that doesn't requi

I can't install skype no matter what I do

Daniel Santos I'm trying to install the Skype 32bit version, I've also tried the 64bit version, but after clicking "Install" the button changes to "Install", but after a few seconds it changes back to installed. I've been researching for hours and can't instal

I can zoom in on the parallax image no matter what I do

colorado powder This question seems to have been asked multiple times on different sites without a real "aha!" answer. I'm still pretty new and I know there are a hundred different ways to code this, but I'm hoping for a very simple solution that doesn't requi

I can zoom in on the parallax image no matter what I do

colorado powder This question seems to have been asked multiple times on different sites without a real "aha!" answer. I'm still pretty new and I know there are a hundred different ways to code this, but I'm hoping for a very simple solution that doesn't requi

I can zoom in on the parallax image no matter what I do

colorado powder This question seems to have been asked multiple times on different sites without a real "aha!" answer. I'm still pretty new and I know there are a hundred different ways to code this, but I'm hoping for a very simple solution that doesn't requi

JavaScript files won't update no matter what I do

pull: I have an external JavaScript file, and it doesn't update whether in FireFox or Chrome, with all browsing data cleared or not. I believe something happened when the file was backed up , I just added "_thedate" to the end of the name. Then save as back to

Java controller always returns 404 no matter what I do

Brain Bytes: I took an example from spring boot documentation and for some reason the java controller always returns 404 This is what I tried package com.example.accessingdatamysql; import org.springframework.beans.factory.annotation.Autowired; import org.spr

No matter what I do, SSH permission is denied (public key)

pristine rock Just when I thought I had a complete understanding of how SSH works, and what didn't, it randomly stopped working again. My permissions are perfect on both computers. One is a Debian machine and the other is a Windows machine using WSL .ssh = 700

DividerItemDecoration won't work no matter what I do

Mihir Kandoi: Here is the code to set the decoration:- recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.HORIZONTAL)); Here is the xml:- <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http:

JavaScript files won't update no matter what I do

pull: I have an external JavaScript file, and it doesn't update whether in FireFox or Chrome, with all browsing data cleared or not. I believe something happened when the file was backed up , I just added "_thedate" to the end of the name. Then save as back to

No matter what I do, Visual Studio connects to TFS using 8080

Jordan No matter what I do, Visual Studio 2017 maintains the HTTP connection to TFS. Our TFS servers have recently moved to SSL/HTTPS connections. If I disconnect and reconnect to: https://tfs.myorg.com:443/tfs The connection becomes: http://tfs.myorg.com:8080