How to customize a NSPopUpButton and its NSMenu?

691 Views Asked by At

I want to style a NSPopUpButton with my own colors. I've gotten pretty much everything else to work except for the caps at the top and bottom of the menu and I can't get the NSPopUpButton to show an image. Here are a few screenshots of the problem:

enter image description here

Why is the drawn background bigger on my custom view compared to the system NSPopUpButton?

Here is an image of the caps problem:

enter image description here

I can't figure out where those caps are drawn and how I can change their color to match the menu items?

View controller

import Cocoa

let textColor = NSColor(calibratedWhite: 0.9, alpha: 1)
let surfacePrimaryColor = NSColor(calibratedWhite: 0.1, alpha: 1)
let surfaceSecondaryColor = NSColor(calibratedWhite: 0.3, alpha: 1)

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.translatesAutoresizingMaskIntoConstraints = false

        let stackView = NSStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        
        let cell = PopUpButtonCell()
        cell.imagePosition = .imageLeading
        let icon = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)
        cell.image = icon
        print("cell.image: \(cell.image)")
        
        let popUpButton = NSPopUpButton()
        popUpButton.cell = cell
        
        for title in (Array(1...100).map { "Folder \($0)" }) {
            let menuItem = NSMenuItem()
            menuItem.title = title
            
            let menuItemView = MenuItemView()
            menuItemView.translatesAutoresizingMaskIntoConstraints = false
            
            menuItemView.onAction {
                cell.title = title
                menuItem.menu?.cancelTracking()
            }
            
            menuItem.view = menuItemView
            
            let titleLabel = NSTextField(string: title)
            titleLabel.drawsBackground = false
            titleLabel.isBezeled = false
            titleLabel.isSelectable = false
            titleLabel.isEditable = false
            titleLabel.maximumNumberOfLines = 1
            titleLabel.textColor = textColor
            
            let deleteButton = Button(systemSymbolName: "xmark")
            deleteButton.font = NSFont.systemFont(ofSize: 14)
            deleteButton.isBordered = false
            deleteButton.contentTintColor = textColor
            
            deleteButton.onAction {
                popUpButton.removeItem(withTitle: title)
            }
            
            let menuItemStackView = NSStackView()
            menuItemView.addSubview(menuItemStackView)
            menuItemStackView.orientation = .horizontal
            menuItemStackView.edgeInsets = NSEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
            menuItemStackView.translatesAutoresizingMaskIntoConstraints = false
            menuItemStackView.leadingAnchor.constraint(equalTo: menuItemView.leadingAnchor).isActive = true
            menuItemStackView.trailingAnchor.constraint(equalTo: menuItemView.trailingAnchor).isActive = true
            menuItemStackView.topAnchor.constraint(equalTo: menuItemView.topAnchor).isActive = true
            menuItemStackView.bottomAnchor.constraint(equalTo: menuItemView.bottomAnchor).isActive = true
            menuItemStackView.addView(titleLabel, in: .leading)
            menuItemStackView.addView(deleteButton, in: .trailing)
            popUpButton.menu?.addItem(menuItem)
        }

        let popUpButton2 = NSPopUpButton()
        popUpButton2.addItems(withTitles: Array(1...100).map { "File \($0)" })
        
        stackView.addArrangedSubview(popUpButton)
        stackView.addArrangedSubview(popUpButton2)
    }
}

Custom button with onAction closure

import AppKit

typealias Listener = () -> Void

class Button: NSButton {
    private var listener: Listener?
    
    init(systemSymbolName: String) {
        super.init(frame: .zero)
        image = NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)
        target = self
        action = #selector(actionPerformed(_:))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    @objc func actionPerformed(_ sender: AnyObject) {
        listener?()
    }
    
    
    func onAction(_ closure: @escaping Listener) {
        listener = closure
    }
}

Custom popup button cell

import Cocoa

class PopUpButtonCell: NSPopUpButtonCell {
    
    override var controlView: NSView? {
        didSet {
            controlView?.wantsLayer = true
            controlView?.layer?.backgroundColor = surfaceSecondaryColor.cgColor
            controlView?.layer?.cornerRadius = 4
        }
    }
    
    // Prevent system background drawing
    override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {
    }
    
    override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
        let attributedTitle = NSMutableAttributedString(attributedString: title)
        let range = NSMakeRange(0, attributedTitle.length)
        attributedTitle.addAttributes([NSAttributedString.Key.foregroundColor : textColor], range: range)

        return super.drawTitle(attributedTitle, withFrame: frame, in: controlView)
    }
}

Why is image nil after setting it on the NSPopUpButton?

How can I change the color of the menu caps?

1

There are 1 best solutions below

3
On

Why is image nil after setting it on the NSPopUpButton?

See setImage:

This method has no effect. The image displayed in a pop up button is taken from the selected menu item (in the case of a pop up menu) or from the first menu item (in the case of a pull-down menu).