I am trying to create a custom NSTextBlock, much like the one Apple did at WWDC 18 (23 mins in).
Okay, so it works great when I'm editing and marking a paragraph with my paragraph style that has the text block attached.
But when I cut and paste it (or archive/unarchive from disk), it loses it. EDIT: It actually turns my TweetTextBlock
subclass into a NSTableViewTextBlock
, which also explains the borders.
Implementation
Here's a full Xcode project. Use the Format
top menu item to trigger the markTweet
function.
Here's how I add the attributes to the paragraph
@IBAction func markTweet(_ sender : Any?){
print("now we are marking")
let location = textView.selectedRange().location
guard let nsRange = textView.string.extractRange(by: .byParagraphs, at: location) else { print("Not in a paragraph"); return }
let substring = (textView.string as NSString).substring(with: nsRange)
let tweetParagraph = NSMutableParagraphStyle()
tweetParagraph.textBlocks = [TweetTextBlock()]
let twitterAttributes : [AttKey : Any] = [
AttKey.paragraphStyle : tweetParagraph,
AttKey.font : NSFont(name: "HelveticaNeue", size: 15)
]
textView.textStorage?.addAttributes(twitterAttributes, range: nsRange)
}
And this is my NSTextBlock
subclass
import Cocoa
class TweetTextBlock: NSTextBlock {
override init() {
super.init()
setWidth(33.0, type: .absoluteValueType, for: .padding)
setWidth(70.0, type: .absoluteValueType, for: .padding, edge: .minX)
setValue(100, type: .absoluteValueType, for: .minimumHeight)
setValue(300, type: .absoluteValueType, for: .width)
setValue(590, type: .absoluteValueType, for: .maximumWidth)
backgroundColor = NSColor(white: 0.97, alpha: 1.0)
}
override func drawBackground(withFrame frameRect: NSRect, in controlView: NSView,
characterRange charRange: NSRange, layoutManager: NSLayoutManager) {
let frame = frameRect
let fo = frameRect.origin
super.drawBackground(withFrame: frame, in: controlView, characterRange:
charRange, layoutManager: layoutManager)
// draw string
let context = NSGraphicsContext.current
context?.shouldAntialias = true
let drawPoint: NSPoint = CGPoint(x: fo.x + 70, y: fo.y + 10)
let nameAttributes = [AttKey.font: NSFont(name: "HelveticaNeue-Bold", size: 15), .foregroundColor: NSColor.black]
var handleAttributes = [AttKey.font: NSFont(name: "HelveticaNeue", size: 15), .foregroundColor: NSColor(red: 0.3936756253, green: 0.4656872749, blue: 0.5323709249, alpha: 1)]
let nameAStr = NSMutableAttributedString(string: "Johanna Appleseed", attributes: nameAttributes)
let handleAStr = NSAttributedString(string: " @johappleseed · 3h", attributes: handleAttributes)
nameAStr.append(handleAStr)
nameAStr.draw(at: drawPoint)
let im = NSImage(named: "profile-twitter")!
im.draw(in: NSRect(x: fo.x + 10, y: fo.y + 10, width: 50, height: 50))
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
What I tried
My thinking is that this might happen because TextKit doesn't know how to archive the attributes from the custom block. But I tried overriding init:fromCoder
and encode
. They don't get called. Not on copy, paste, archiving, unarchiving. So I suppose that was not it. This leads me to think that all this custom drawing logic can't be saved in an attributed string, and that this is all happening in the layout manager. That makes sense. But how do I persist the block, then?
UPDATE: I tried reading the attributes. It has a paragraph style, and that paragraph style has an item in the textBlocks
array property. But that text block is an NSTextBlock
and not my subclass (i tried if block is TweetTextBlock
which returns false)
UPDATE 2: I tried overriding properties like classForArchiver
, and then reading them with e.g. print("twb: Class for archiver", block.classForArchiver)
. What's interesting here is that the text block has been turned into a NSTextTableBlock
! I'm so deep in hacking this now that I'm looking for a way to store the className somewhere in the text block. So far, the only one I can think of is the tooltip
property, but that's visible to the user, and I might want to use that for something else.
UPDATE 3: The tooltip is also not preserved. That's weird. The next big hack I can think of is setting the text color to HSB (n, 0, 0), where n is the identifier for the NSTextBlock subclass. Let's hope I don't have to go there.
UPDATE 4. This is most likely caused by both archiving and copy/pasting transforms the string into RTF. Here's public.rtf
from my clipboard
{\rtf1\ansi\ansicpg1252\cocoartf2509
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;}
{\colortbl;\red255\green255\blue255;\red245\green245\blue245;}
{\*\expandedcolortbl;;\csgray\c97000;}
\pard\intbl\itap1\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\f0\fs30 \cf0 THIS text is in a TweetTextBlock}