In Safari, I can drag an item (like a URL or even a .webloc bookmark from the finder) right onto a tab to open.
How do I make the window tab bar item view a drop target in my own AppKit app?
I’d like to be able to accept dropping an NSPasteboard
similar to how NSView
instances can:
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
// Handle drop
}
But the tab bar and the contained NSTabButton
instances are provided by the system.
Subclassing or extending NSTabButton
doesn’t seem to work because it’s private.
Brian Webster’s brilliant answer (thank you!) inspired this solution.
I added the demo code to a fully working project on GitHub.
First, we add a custom accessory view to the window
’s tab
when creating the window. We pass a reference to the NSWindowController
so that we can easily notify it whenever something was dropped on the tab item.
window.tab.accessoryView = TabAccessoryView(windowController: windowController)
This custom accessoryView
(TabAccessoryView
) is not the view that will accept the drops, because the accessory view is confined to an NSStackView
, together with the close button and the title label, covering only a portion of the tab next to the title label.
So instead, we use the fact that the accessoryView
is part of the NSTabButton
’s view hierarchy to inject another custom view (TabDropTargetView
) behind the NSStackView
…
class TabAccessoryView: NSView {
weak private(set) var windowController: NSWindowController?
private let tabDropTargetView: TabDropTargetView
init(windowController: NSWindowController? = nil) {
self.windowController = windowController
self.tabDropTargetView = TabDropTargetView(windowController: windowController)
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidMoveToWindow() {
guard tabDropTargetView.superview == nil else { return }
// DEBUG: Highlight accessory view
wantsLayer = true
layer?.backgroundColor = NSColor.red.withAlphaComponent(0.1).cgColor
// The NSTabButton close button, title, and accessory view are contained in a stack view:
guard let stackView = superview as? NSStackView,
let backgroundView = stackView.superview else { return }
// Add the drop target view behind the NSTabButton’s NSStackView and pin it to the edges
backgroundView.addSubview(tabDropTargetView, positioned: .below, relativeTo: stackView)
tabDropTargetView.translatesAutoresizingMaskIntoConstraints = false
tabDropTargetView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor).isActive = true
tabDropTargetView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor).isActive = true
tabDropTargetView.topAnchor.constraint(equalTo: backgroundView.topAnchor).isActive = true
tabDropTargetView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor).isActive = true
}
}
… which will handle the dropped item:
class TabDropTargetView: NSView {
private(set) weak var windowController: NSWindowController?
let allowedDropTypes: Array<NSPasteboard.PasteboardType> = [.URL, .fileContents, .string, .html, .rtf]
init(windowController: NSWindowController? = nil) {
self.windowController = windowController
super.init(frame: .zero)
// Tell the system that we accept drops on this view
registerForDraggedTypes(allowedDropTypes)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidMoveToWindow() {
// DEBUG: Highlight drop target view
wantsLayer = true
layer?.backgroundColor = NSColor.green.withAlphaComponent(0.05).cgColor
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return .copy
}
override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
return .copy
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
// Optional: Ignore drags from the same window
guard (sender.draggingSource as? NSView)?.window != window else { return false }
// Check if the dropped item contains text:
let pasteboard = sender.draggingPasteboard
guard let availableType = pasteboard.availableType(from: allowedDropTypes),
let text = pasteboard.string(forType: availableType) else {
return false
}
if let windowController = windowController as? WindowController {
// Use the reference to the tab’s NSWindowController to pass the dropped item
windowController.handleDroppedText(text)
}
return true
}
}