NSMenuItem with custom view disappears while scrolling

548 views Asked by At

I implement a NSMenu with NSMenuItem and set custom view to it. When menu is scrollable, mouse hovering on ▼ button to scroll will cause some menuItem disappear (or not draw correctly). Hope someone give me some help. I will appreciate that.

Here's video about this issue: https://streamable.com/obrbon

Here's my code:

private func setupMenuItemView(_ menu: NSMenu) {
    let menuItemHeight: CGFloat = 20
    let menuWidth = frame.width
    let textFieldPadding: CGFloat = 10
    for menuItem in menu.items {
        guard !menuItem.title.isEmpty else { continue }
        let menuItemView = MenuItemView(frame: NSRect(x: 0, y: 0, width: Int(menuWidth), height: menuItemHeight))
        let textField = MenuItemTextField(labelWithString: menuItem.title)
        textField.frame = NSRect(
            x: textFieldPadding,
            y: (menuItemView.frame.height-textField.frame.height)/2,
            width: menuWidth-textFieldPadding*2,
            height: textField.frame.height
        )
        textField.lineBreakMode = .byTruncatingTail
        menuItemView.addSubview(textField)
        menuItemView.toolTip = menuItem.title
        menuItem.view = menuItemView
        menuItem.target = self
        menuItem.action = #selector(onMenuItemClicked(_:))
    }
}

fileprivate class MenuItemView: NSView {
    override func mouseUp(with event: NSEvent) {
        guard let menuItem = enclosingMenuItem else { return }
        guard let action = menuItem.action else { return }
        NSApp.sendAction(action, to: menuItem.target, from: menuItem)
        menuItem.menu?.cancelTracking()
    }

    override func draw(_ dirtyRect: NSRect) {
        guard let menuItem = enclosingMenuItem else { return }
        if menuItem.isHighlighted {
            NSColor.alternateSelectedControlColor.set()
        } else {
            NSColor.clear.set()
        }
        NSBezierPath.fill(dirtyRect)
        super.draw(dirtyRect)
    }
}

fileprivate class MenuItemTextField: NSTextField {
    override var allowsVibrancy: Bool {
        return false
    }
}

After calling setupMenuItemView(), i call menu.popup(). Hope this information helps.

2

There are 2 answers

0
apodidae On

I was unable to get your posted code to work correctly. The demo below is an alternative which uses a popUpContextual menu with subclassed text fields embedded in the menuItem views (note that the view associated with each menuItem is used and a custom view class is not created). Text alignment and truncation is functional. Menu width is also flexible and may be set to match width of the menu title field. The demo may be run in Xcode by copy/pasting source code into a newly added ‘main.swift’ file and additionally deleting Apple’s AppDelegate class.

import Cocoa

var view = [NSTextField]()

class TextField: NSTextField {

override func mouseDown(with event: NSEvent) {
 print("selected = \(self.tag)")
 for i:Int in 0..<view.count {
  if(self.tag == i) {
   view[i].backgroundColor = .lightGray
   } else {
   view[i].backgroundColor = .clear
  }
 }
}

}

class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var menu = NSMenu()

let _menuLeft : CGFloat = 40
let _menuTop : CGFloat = 70
let _menuWidth : CGFloat = 70
let _menuItemH : CGFloat = 20

@objc func menuBtnAction(_ sender:AnyObject ) { 
let menuOrigin = NSMakePoint(_menuLeft, sender.frame.origin.y - 5)
let wNum : Int = sender.window.windowNumber
let event  = NSEvent.mouseEvent(with:.leftMouseDown, location:menuOrigin, modifierFlags:[], timestamp:0, windowNumber:wNum, context:nil, eventNumber:0, clickCount:1, pressure:1.0)
NSMenu.popUpContextMenu(menu, with: event!, for: window.contentView!)
}

func buildMenu() {
 let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
}
    
func buildWnd() {
    
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300

 window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
 window.center()
 window.title = "Swift Test Window"
 window.makeKeyAndOrderFront(window)

let menuItems = ["10%","25%","50%","75%","100%","Longer text."]
menu.title = "Fit"
var count:Int = 0
for mItem in menuItems{
 let menuItem = NSMenuItem()
 menu.addItem(menuItem)
 let textField = TextField(frame:NSMakeRect(0,0,_menuWidth, _menuItemH))
 menuItem.view = textField
 textField.alignment = .left // .left, .center, .right
 textField.stringValue = mItem
 textField.lineBreakMode = .byTruncatingTail
 textField.backgroundColor = .clear
 textField.isEditable = false
 textField.tag = count
 textField.isBordered = false
 textField.font = NSFont( name:"Menlo bold", size:14 )
 count = count + 1
 view.append(textField) 
}

// **** Menu title **** //
let label = NSTextField (frame:NSMakeRect( _menuLeft, _wndH - 50, _menuWidth, 24 ))
 window.contentView!.addSubview (label)
 label.autoresizingMask = [.maxXMargin,.minYMargin]
 label.backgroundColor = .clear
 label.lineBreakMode = .byTruncatingTail
 label.isSelectable = false
 label.isBordered = true
 label.font = NSFont( name:"Menlo bold", size:14 )
 label.stringValue = menu.title

// **** Menu Disclosure Button **** //
let menuBtn = NSButton (frame:NSMakeRect( (_menuLeft + _menuWidth) - 20, _wndH - 50, 20, 24 ))
 menuBtn.bezelStyle = .disclosure
 menuBtn.autoresizingMask = [.maxXMargin,.minYMargin]
 menuBtn.title = ""
 menuBtn.action = #selector(self.menuBtnAction(_:))
 window.contentView!.addSubview (menuBtn)

// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)
}
 
func applicationDidFinishLaunching(_ notification: Notification) {
 buildMenu()
 buildWnd()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
 return true
}

}
let appDelegate = AppDelegate()

// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()

0
apodidae On

Second possible alternative utilizes an NSPopUpButton with drop down menu which accommodates DarkMode by changing text color. As before, a subclassed text field is embedded into each menuItem.view to support text truncation and alignment. May be run in Xcode with instructions given previously.

import Cocoa

var view = [NSTextField]()

class TextField: NSTextField {

override func mouseDown(with event: NSEvent) {
 print("selected = \(self.tag)")
 for i:Int in 0..<view.count {
  if(self.tag == i) {
   view[i].backgroundColor = .lightGray
   } else {
   view[i].backgroundColor = .windowBackgroundColor
  }
 }
}

}

class AppDelegate: NSObject, NSApplicationDelegate {
var window:NSWindow!
var menu:NSMenu!
var pullDwn:NSPopUpButton!
var count:Int = 0

let _menuItemH : CGFloat = 20

func isDarkMode(view: NSView) -> Bool {
 if #available(OSX 10.14, *) {
  return view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
 }
 return false
}

@objc func myBtnAction(_ sender:Any ) {
 print(pullDwn.index(of:sender as! NSMenuItem))
}

func buildMenu() {
 let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
}
    
func buildWnd() {
    
let _wndW : CGFloat = 400
let _wndH : CGFloat = 300

 window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
 window.center()
 window.title = "Swift Test Window"
 window.makeKeyAndOrderFront(window)

// **** NSPopUpButton with Menu **** //
let menuItems = ["10%","25%","50%","75%","100%","200%","300%","400%","800%","longertext"]
pullDwn = NSPopUpButton(frame:NSMakeRect(80, _wndH - 50, 80, 30), pullsDown:true)
pullDwn.autoresizingMask = [.maxXMargin,.minYMargin]
let menu = pullDwn.menu
for mItem in menuItems{
 let menuItem = NSMenuItem()
 menu?.addItem(menuItem)
 menuItem.title = "Fit"
 let textField = TextField(frame:NSMakeRect( 0, 0, pullDwn.frame.size.width, _menuItemH))
 menuItem.view = textField
 textField.alignment = .left // .left, .center, .right
 textField.stringValue = mItem
 textField.lineBreakMode = .byTruncatingTail
 if (isDarkMode(view: textField)){
  textField.textColor = .white
 } else {
  textField.textColor = .black
 }
 textField.backgroundColor = .windowBackgroundColor
 textField.isEditable = false
 textField.tag = count
 textField.isBordered = false
 textField.font = NSFont( name:"Menlo", size:14 )
 count = count + 1
 view.append(textField) 
}
window.contentView!.addSubview (pullDwn)

// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)
}
 
func applicationDidFinishLaunching(_ notification: Notification) {
 buildMenu()
 buildWnd()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
 return true
}

}
let appDelegate = AppDelegate()

// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()