swiftappkitnstextviewtextkit

(AppKit) Tab insertion inside of NSTextBlock


I'm working on a markdown editor for macOS using AppKit, and have all the basics down and working. I'm using NSTextBlock for the code blocks.

One issue I'm encountering however, is that typing a Tab inside of an NSTextBlock, causes the caret to move down to the next paragraph, as opposed to inserting the tab whitespace. The desired outcome is for the user to be able to insert tabs inside of NSTextBlock.

I've created a sample project to demonstrate this behavior on GitHub.

To quickly go over the sample project:

It's a simple AppKit application. I've added an Editable, Scrollable Text View to the default ViewController in Main.storyboard. The textView is connected to the ViewController class via an IBOutlet.

Inside of the autogenerated viewDidLoad() method, I call the insertText function, which I wrote to insert a line of text, followed by two paragraphs styled with NSTextBlocks, into the NSTextView.

  func insertText(textStorage: NSTextStorage) {

    //Insert first line of regular text
    var attributes: [NSAttributedString.Key: Any] = [
        .font: NSFont.systemFont(ofSize: 24),
        .foregroundColor: NSColor.textColor,
        .backgroundColor: NSColor.clear
    ]
    let attributedString1: NSAttributedString = NSAttributedString(string: "Test\n", attributes: attributes)
    textStorage.append(attributedString1)
    
    //Insert the blue block holdig text
    attributes[.foregroundColor] = NSColor.white
    attributes[.paragraphStyle] = ParagraphStyle(bgColor: NSColor.systemBlue).paragraphStyle
    let attributedBlock1 = NSAttributedString(string: "Hello, I'm the blue block\n", attributes: attributes)
    textStorage.append(attributedBlock1)

    //Insert the pink block holdig text
    attributes[.foregroundColor] = NSColor.black
    attributes[.paragraphStyle] = ParagraphStyle(bgColor: NSColor.systemPink).paragraphStyle
    let attributedBlock2 = NSAttributedString(string: "Hello, I'm the pink block\n", attributes: attributes)
    textStorage.append(attributedBlock2)
}

The effect this has:

enter image description here

If you launch the application and try to insert a Tab when the caret is located at the beginning of the first paragraph, Tab whitespace is added to the beginning of this paragraph as expected. However, if you try to do the same to the paragraphs styled with NSTextBlocks, the caret simply moves down to the next paragraph, and Tab whitespace is not inserted.

The other two classes I wrote for the sample project are ParagraphStyle and CustomTextBlock.

For the ParagraphStyle class, holding the paragraph style that was used for the attributes of the blue and pink blocks:

class ParagraphStyle {

let bgColor: NSColor
let paragraphStyle: NSParagraphStyle

init(bgColor: NSColor) {
    self.bgColor = bgColor
    //Set paragraph style
    self.paragraphStyle = {
        let mutableParagraphStyle = NSMutableParagraphStyle()
        let specialBlock = CustomTextBlock(bgColor: bgColor)
        mutableParagraphStyle.textBlocks.append(specialBlock)
        let style = mutableParagraphStyle as NSParagraphStyle
        return style
    }()
}}

For the CustomTextBlock class, inheriting from NSTextBlock, I define the color and dimensions of the block:

class CustomTextBlock: NSTextBlock {
        
init(bgColor: NSColor) {
    super.init()
    //Control the BG color of the text block
    self.backgroundColor = bgColor
    //Control dimensions of the text block
    self.setValue(100, type: NSTextBlock.ValueType.percentageValueType, for: NSTextBlock.Dimension.width)
    self.setValue(50, type: NSTextBlock.ValueType.absoluteValueType, for: NSTextBlock.Dimension.minimumHeight)
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}}

What I've tried

  1. Adding NSTextTabs to the .tabStops array when initializing the value for the paragraphStyle property of the ParagraphStyle class.
  2. Adding NSTextTab via the addTabStop method.
  3. Setting a value to defaultTabInterval
  4. Setting the textView's nextKeyView and nextResponder to nil.

The only thing remaining (that I can think of), is to listen for the user's pressing of the tab key when the textview is first responder. When the event occurs, I would programmatically insert Tab white space where the user's caret is, and then programmatically move the caret back to where it should be. This however is an extremely round-about way of fixing this seemingly simply issue, which is why I believe I'm probably missing something obvious. Was hoping that the more experienced developers on here might have other suggestions!

Thank you in advance!


Solution

  • Apparently a tab in a text block moves the insertion point to the next text block, which makes sense in a table. Workaround: subclass NSTextView, override insertTab and insert a tab.

    override func insertTab(_ sender: Any?) {
        insertText("\t", replacementRange: selectedRange())
    }