NSTextView (Plain Text) and the Pasteboard: PasteboardType.string Is Not Handled

For a plain text (not rich text/RTF) NSTextView, I found that:

  • writablePasteboardTypes contains only "NSStringPboardType" by default.
  • readablePasteboardTypes contains a lot of types, but only "NSStringPboardType" for plain text copying.-
    An NSPasteboard understands both "NSStringPboardType" and the NSPasteboard.PasteboardType.string value.

Possible Fixes

Since NSTextView doesn’t understand the NSPasteboard.PasteboardType.string pasteboard type for reading or writing, I tried two approaches to handle plain text input (pasting) and output (cut/copy):

  1. Add a backport for the deprecated raw value, "NSStringPboardType";
  2. Patch NSPasteboard.PasteboardType.string into the list of readable/writable types.

The backport seems to work, but extending supported types sounds like more robust solution.

Expand readable and writable pasteboard types

To give the “new” (since masOS 10.6) pasteboard type higher precedence, prepend it to the default types:

class MyTextView: NSTextView {
    override var readablePasteboardTypes: [NSPasteboard.PasteboardType] {
        return [NSPasteboard.PasteboardType.string]
            + super.readablePasteboardTypes
    }

    override var writablePasteboardTypes: [NSPasteboard.PasteboardType] {
        return [NSPasteboard.PasteboardType.string]
            + super.writablePasteboardTypes
    }
}

In my subclass, I’m not re-computing this on the fly, but cache the resulting array.

When you add a breakpoint to NSTextView’s writeSelection(to:type:) and inspect the type that has been picked, this approach will set the type parameter to NSPasteboard.PasteboardType.string if applicable.

Fair warning: I haven’t tested if forwarding to super works for all imaginable cases. My personal intention is to handle the .string case separately and replace the system default behavior. So I’m fine with that caveat.

Backport of "NSStringPboardType"

The Objective-C constant is not exposed to Swift, so you need to initialize a new NSPasteboard.PasteboardType:

override func writeSelection(
    to pasteboard: NSPasteboard,
    type: NSPasteboard.PasteboardType
) -> Bool {
    switch type {
    case .string,  // Actual raw value of "public.utf8-plain-text"
         .init(rawValue: "NSStringPboardType"):
        // Do something with the string
    default:
        return super.writeSelection(to: pasteboard, type: type)
    }
}

I’m actually using a static constant to avoid typos in the string wherever needed, and to add a doc comment:

extension NSPasteboard.PasteboardType {
    /// Backport of the value since Objective-C days that is used when copying from a `NSTextView`.
    ///
    /// According to the header files, the Objective-C `NSStringPboardType` constant should've been replaced with `NSPasteboardTypeString` since macOS 10.6, but the `rawValue` of the .string case accessible to Swift is "public.utf8-plain-text". These don't match.
    static let _stringLegacy: NSPasteboard.PasteboardType = .init(rawValue: "NSStringPboardType")
}

In fact, I’m combining both approaches, so I’m prepending the .string pasteboard type to the arrays of acceptable types, and I use .string and ._stringLegacy in my switch-case statements. I don’t understand the edge cases here, so I’m rather safe than sorry.

Observations

These are more or less the raw observations I made. It’s my lab notes so you can compare with your findings, and understand how I come to my conclusions.

NSStringPboardType is not bridged to Swift

Objective-C’s NSStringPboardType was deprecated in macOS 10.14, but the replacement NSPasteboardTypeString is available since macOS 10.6. – Keep this in mind why some collections contain both (for legacy compatibility reasons most likely).

NSPasteboardTypeString is bridged to Swift via NSPasteboard.PasteboardType.string. Its rawValue is public.utf8-plain-text, which is different fromNSStringPboardType.

APPKIT_EXTERN NSPasteboardType const NSPasteboardTypeString	 		API_AVAILABLE(macos(10.6)); // Replaces NSStringPboardType
// ...
APPKIT_EXTERN NSPasteboardType NSStringPboardType API_DEPRECATED_WITH_REPLACEMENT("NSPasteboardTypeString", macos(10.0,10.14));

NSPasteboard comes with multiple string representations

Inspecting a pasteboard when ⌘V pasting plain text reveals that both the new .string type and the old NSStringPboardType is accessible, so you can use pasteboard.string(forType: .string) without problems.

# At breakpoint in readSelection(from:type:)
(lldb) po pasteboard.types
▿ Optional<Array<NSPasteboardType>>
  ▿ some : 2 elements
    ▿ 0 : NSPasteboardType
      - _rawValue : public.utf8-plain-text
    ▿ 1 : NSPasteboardType
      - _rawValue : NSStringPboardType

That’s the only place where this works as expected, since the text view itself doesn’t understand the .string value.

NSTextView supports a lot of types, but not NSPasteboardType.PasteboardType.string

Pasting plain text goes through the legacy API of NSStringPboardType. That’s the only writable (cut/copy) type, and it’s not equal to NSPasteboardType.PasteboardType.string:

(lldb) po myTextView.writablePasteboardTypes
▿ 1 element
  ▿ 0 : NSPasteboardType
    - _rawValue : NSStringPboardType

Reading is more versatile, but also only lists the legacy type, not NSPasteboardType.PasteboardType.string:

(lldb) po myTextView.readablePasteboardTypes
▿ 9 elements
  ▿ 0 : NSPasteboardType
    - _rawValue : NSStringPboardType
  ▿ 1 : NSPasteboardType
    - _rawValue : NeXT Rich Text Format v1.0 pasteboard type
  ▿ 2 : NSPasteboardType
    - _rawValue : NeXT RTFD pasteboard type
  ▿ 3 : NSPasteboardType
    - _rawValue : Apple HTML pasteboard type
  ▿ 4 : NSPasteboardType
    - _rawValue : WebURLsWithTitlesPboardType
  ▿ 5 : NSPasteboardType
    - _rawValue : public.url
  ▿ 6 : NSPasteboardType
    - _rawValue : Apple URL pasteboard type
  ▿ 7 : NSPasteboardType
    - _rawValue : NSFilenamesPboardType
  ▿ 8 : NSPasteboardType
    - _rawValue : NeXT ruler pasteboard type