Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I accept a drop for a dragged item on a window tab in macOS/AppKit?

Mouse cursor dragging bookmark file to a Safari window tab

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.

like image 602
Frank R Avatar asked Dec 19 '25 21:12

Frank R


1 Answers

Brian Webster’s brilliant answer (thank you!) inspired this solution.

Window with tabs that can accept a drop

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
    }
}
like image 166
Frank R Avatar answered Dec 21 '25 11:12

Frank R