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.-
AnNSPasteboard
understands both"NSStringPboardType"
and theNSPasteboard.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):
- Add a backport for the deprecated raw value,
"NSStringPboardType"
; - 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