NSMenu Subclass does not respond to mouse click

53 Views Asked by At

I am subclassing an NSPopUpButton with the purpose of having control over the drawing methods of the button itself, but also the NSMenu that will pop up. Therefore I am also subclassing NSMenu and - most importantly - setting the view of each menu item to a custom NSView.

So far I have managed to come very close to the appearance of the original NSPopupButton and its menu. In the code, I provide a small window that will display an original NSButton on the left side and an instance of my custom version on the right.

However, the custom menu does not function properly. The following issues occur:

  1. The button can be clicked and the menu will pop up. When the mouse is moved inside the menu, the item on which the pointer is hovering will highlight properly, except for the item that is selected: when the mouse exits its tracking area to the neighboring item, this one will be highlighted, but the first one will not lose highlight color. Only when entering the selected item again and then exiting it a second time it will lose the highlight properly.

  2. Clicking an item will NOT dismiss the menu, the menu does not respond to any click within one of its items. The menu will however be dismissed when a click outside the menu occurs.

  3. The button and the menu are fully functional when using the keyboard: Tab switches between the standard and the custom PopUpButton, space will summon the menu, the arrow buttons move the selection, and space or return will make a selection and dismiss the menu.

  4. The first menu entry (Item 1) can not be selected, when dismissing the menu with enter or space when Item 1 is highlighted the Item that was selected before will stay selected.

Problem 4 is possibly unrelated, my main question is:

Why do the CustomMenuItemViews not respond to mouse events the way a stock Menu does? I assume that there is either a method that I have to override, a delegate that has to be set somewhere, or both, but I have not yet managed to find the part of the code where I have to hook in.

I was at least able to pinpoint the problem to the overridden method willOpenMenu - if I do not override, I get normal behavior, but of course, the menu will then be drawn by the cocoa method.

import Cocoa
import AppKit


@main
class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        window.contentViewController = MyViewController(size: NSSize(width: 200, height: 80))
    }
}

class MyViewController: NSViewController {
    
    public init(size: NSSize) {
        super.init(nibName: nil, bundle: nil)
        self.view = MyInnerView(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class MyInnerView: NSView, NSMenuDelegate {
    
    public override init(frame: NSRect) {
        
        super.init(frame: frame)
        
        let standardPopUp = NSPopUpButton(title: "Switch", target: nil, action: nil)
        standardPopUp.frame = NSRect(x: 10, y: constant.buttonFrameY, width: 80, height: constant.buttonFrameHeigth)
        standardPopUp.addItems(withTitles: ["Item 1", "Item 2", "Item 3"])
        
        let popUpCell = CustomPopUpButtonCell()
        
        let customPopUp = CustomPopUpButton(title: "Switch", target: nil, action: nil)
        customPopUp.cell = popUpCell
        customPopUp.menu = CustomPopUpMenu()
        customPopUp.menu?.delegate = self
        
        customPopUp.frame = NSRect(x: 90, y: constant.buttonFrameY, width: 80, height: constant.buttonFrameHeigth)
        customPopUp.addItems(withTitles: ["Item 1", "Item 2", "Item 3"])
      
        self.addSubview(standardPopUp)
        self.addSubview(customPopUp)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class CustomPopUpButton: NSPopUpButton {
    
    override func drawFocusRingMask() {
        // prevent focus ring drawing
    }
    
    override func becomeFirstResponder() -> Bool {
        (self.cell as! CustomPopUpButtonCell).hasFocus = true
        self.needsDisplay = true
        return true
    }
    
    override func resignFirstResponder() -> Bool {
        (self.cell as! CustomPopUpButtonCell).hasFocus = false
        self.needsDisplay = true
        return true
    }
    
    // this function breaks the intended behaviour
    
    override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
       
        for (index,item) in self.menu!.items.enumerated() {
            
            item.view = MenuItemCustomView(frame: NSRect(x: 0, y: 0, width: 150, height: constant.popUpMenuCellHeigth))
            item.view?.menu = menu
            (item.view as! MenuItemCustomView).text = item.title
            
            if self.indexOfSelectedItem == index {
                (item.view as! MenuItemCustomView).selected = true
            }
        }
    }
}

class CustomPopUpMenu: NSMenu {
   
}

class CustomPopUpButtonCell: NSPopUpButtonCell {
    
    var hasFocus = false
  
    override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
        
        let context = NSGraphicsContext.current!.cgContext
       
        // calculate width
        
        let buttonWidth = CGFloat(60)
        
        // draw rounded rect with shadow
        
        let buttonRect = CGRect(x: constant.popUpButtonInset, y: (cellFrame.height/2 - constant.popUpButtonHeigth/2) - constant.popUpButtonVerticalOffset, width: buttonWidth, height: constant.popUpButtonHeigth)
        
        let roundedRect = CGPath.init(roundedRect: buttonRect, cornerWidth: constant.popUpButtonCornerRadius, cornerHeight: constant.popUpButtonCornerRadius, transform: nil)
        
        let shadowColor = CGColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5)
        context.setShadow(
          offset: CGSize(width: 0, height: 0),
          blur: 3.0,
          color: shadowColor)
        context.setLineWidth(3)
        context.setFillColor(.white)
        context.addPath(roundedRect)
        context.fillPath()
        
        context.setShadow(offset: CGSize(), blur: 0)
        
        // draw arrow rect
        
        let arrowRect = CGRect(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth - constant.popUpButtonArrowRectGap, y: (cellFrame.height/2 - constant.popUpButtonArrowRectWidth/2 - constant.popUpButtonVerticalOffset), width: constant.popUpButtonArrowRectWidth, height: constant.popUpButtonArrowRectWidth)
        
        let arrowRoundedRect = CGPath.init(roundedRect: arrowRect, cornerWidth: constant.popUpButtonArrowRectCornerRadius, cornerHeight: constant.popUpButtonArrowRectCornerRadius, transform: nil)
        context.setFillColor(NSColor.controlAccentColor.cgColor)
        context.addPath(arrowRoundedRect)
        context.fillPath()
        
        // draw arrows
        
        context.setStrokeColor(.white)
        context.setLineWidth(1.5)
        context.setLineCap(.round)
        context.move(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth - constant.popUpButtonArrowRectGap + 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset + 2)))
        context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth/2 - constant.popUpButtonArrowRectGap, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset + constant.popUpButtonArrowRectWidth/2 - 3)))
        context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectGap - 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset + 2)))
        context.strokePath()
        
        context.move(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth - constant.popUpButtonArrowRectGap + 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset - 2)))
        context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectWidth/2 - constant.popUpButtonArrowRectGap, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset - constant.popUpButtonArrowRectWidth/2 + 3)))
        context.addLine(to: CGPoint(x: constant.popUpButtonInset + buttonWidth - constant.popUpButtonArrowRectGap - 5, y: (cellFrame.height/2 - constant.popUpButtonVerticalOffset - 2)))
        context.strokePath()
        
        // draw text
        
        let textColor: NSColor = .black
        
        let attributes = [
            NSAttributedString.Key.font : NSFont(name: "Lucida Grande", size: CGFloat(12)),
            NSAttributedString.Key.foregroundColor : textColor
        ]
        
        let textPosition = NSPoint(x: constant.popUpButtonInset + constant.popUpButtonArrowRectGap, y: constant.popUpButtonVerticalOffset + 8 - constant.popUpButtonArrowRectGap)
      
        NSAttributedString(string: self.selectedItem!.title, attributes: attributes as [NSAttributedString.Key : Any]).draw(at: textPosition)
        
        if hasFocus {
            
            let buttonRect = CGRect(x: constant.popUpButtonInset - constant.popUpButtonFocusRingThickness/4, y: (cellFrame.height/2 - constant.popUpButtonHeigth/2) - constant.popUpButtonVerticalOffset - constant.popUpButtonFocusRingThickness/4, width: buttonWidth + constant.popUpButtonFocusRingThickness*0.5, height: constant.popUpButtonHeigth + constant.popUpButtonFocusRingThickness*0.5)
            
            let roundedRect = CGPath.init(roundedRect: buttonRect, cornerWidth: constant.popUpButtonFocusRingCornerRadius, cornerHeight: constant.popUpButtonFocusRingCornerRadius, transform: nil)
            
            context.setLineWidth(constant.popUpButtonFocusRingThickness)
            context.setStrokeColor((NSColor.keyboardFocusIndicatorColor).cgColor)
            context.addPath(roundedRect)
            context.strokePath()
        }
    }
}



class MenuItemCustomView: NSView {
    
    var text: String = ""
    var scaleFactor: CGFloat = 1
    var selected = false
    
    override func draw(_ dirtyRect: NSRect) {
        
        let context = NSGraphicsContext.current!.cgContext
        context.setLineWidth(1)
        
        var textColor: NSColor
        
        if self.enclosingMenuItem!.isHighlighted {
            
            textColor = .white
            context.setStrokeColor(.white
            )
            // draw selection frame
            
            let arrowRect = CGRect(x: constant.popUpMenuSelectionInset, y: 0, width: (self.frame.width - constant.popUpMenuSelectionInset*2), height: self.frame.height)
            
            let arrowRoundedRect = CGPath.init(roundedRect: arrowRect, cornerWidth: constant.popUpButtonArrowRectCornerRadius, cornerHeight: constant.popUpButtonArrowRectCornerRadius, transform: nil)
            context.setFillColor(NSColor.controlAccentColor.cgColor)
            context.addPath(arrowRoundedRect)
            context.fillPath()
            
        } else {
            textColor = .black
            context.setStrokeColor(.black)
        }
        
        let attributes = [
            NSAttributedString.Key.font : NSFont(name: "Lucida Grande", size: CGFloat(12*scaleFactor)),
            NSAttributedString.Key.foregroundColor : textColor
        ]
        
        let textPosition = NSPoint(x: constant.popUpMenuTextX*scaleFactor, y: constant.popUpMenuTextY*scaleFactor)
        
        NSAttributedString(string: self.text, attributes: attributes as [NSAttributedString.Key : Any]).draw(at: textPosition)
        
        if selected {
            
            // draw checkmark
            
            context.setLineWidth(2*scaleFactor)
            
            let inset = constant.popUpMenuSelectionInset
            
            context.move(to: CGPoint(x: (inset + 3)*scaleFactor, y: (self.frame.height/2)))
            context.addLine(to: CGPoint(x: (inset + 7)*scaleFactor, y: self.frame.height*0.3))
            context.addLine(to: CGPoint(x: (inset + 13)*scaleFactor, y: (self.frame.height*0.7)))
            context.strokePath()
        }
    }
}

struct constant {
    
    static let popUpButtonHeigth = CGFloat(20)
    static let popUpButtonInset = CGFloat(4)
    static let popUpButtonCornerRadius = CGFloat(5)
    static let popUpButtonVerticalOffset = CGFloat(1.5)
    static let popUpButtonFocusRingThickness = CGFloat(4)
    static let popUpButtonFocusRingCornerRadius = CGFloat(6)
    static let popUpButtonArrowRectWidth = CGFloat(15)
    static let popUpButtonArrowRectGap = CGFloat(2)
    static let popUpButtonArrowRectCornerRadius = CGFloat(3)
    static let popUpMenuCellHeigth = CGFloat(24)
    static let popUpMenuTextX = CGFloat(25)
    static let popUpMenuTextY = CGFloat(4)
    static let popUpMenuSelectionInset = CGFloat(5)
    
    static let buttonFrameY = CGFloat(10)
    static let buttonFrameHeigth = CGFloat(35)
}

0

There are 0 best solutions below