Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.


Building a popup NSMenu structure programatically in Swift

OK, so the title is a little misleading, while this example does show how to build a popup menu structure programatically I do use the interface builder to associate a menu with the view.

The later can be done in the interface builder by opening up the xib file with the view and adding a NSMenu to the xib file. It will have three menu items by default, I removed two and renamed the only remaining menu item "popup menu for the view" simply as a reminder to myself to know why the menu is in there.

This popup menu needs a name that is unique for the view, I called mine "Popup":

Next, connect the menu to the view by assigning the delegate and outlet (via drag, drop, select):

Now the popup menu is connected to the view and the view is set as the delegate we can override validateMenuItem,  menuNeedsUpdate and menuDidClose of NSView to handle menu operations.

In order to build the popup menu hierarchy we need menuNeedsUpdate.

In the view implement this method like this:

func menuNeedsUpdate(menu: NSMenu) {
    switch menu.title {
    case "Popup":
        // Check if the menu is already correctly initialized
        if menu.itemArray.count == 1 { // When 1, there is the item that was defined in the XIB
            // Get rid of the item that is defined in the XIB
            // Add the default "remove" item
            menu.addItemWithTitle("Remove", action: "removeItem:", keyEquivalent: "")
            // Add a submenu to add items
            let addMenu = NSMenu(title: "Add")
            addMenu.delegate = self
            let addMenuItem = NSMenuItem(title: "Add Item ...", action: nil, keyEquivalent: "")
            menu.setSubmenu(addMenu, forItem: addMenuItem)
            // Add a submenu to convert items
            let convertMenu = NSMenu(title: "Convert")
            convertMenu.delegate = self
            let convertMenuItem = NSMenuItem(title: "Convert Item ...", action: nil, keyEquivalent: "")
            menu.setSubmenu(convertMenu, forItem: convertMenuItem)
    case "Add":
        // Build this menu if there are no items yet
        if menu.itemArray.count == 0 {
            menu.addItemWithTitle("Null", action: "addNull:", keyEquivalent: "")
            menu.addItemWithTitle("Bool", action: "addBool:", keyEquivalent: "")
            menu.addItemWithTitle("Number", action: "addNumber:", keyEquivalent: "")
    case "Convert":
        // This is done dynamically, so get rid of the existing items    


        // Now it depends on what exactly is in this line.
        if let object = document.determineObject() {
            switch object {
            case .NULL:
                menu.addItemWithTitle("To Bool", action: "convertToBool:", keyEquivalent: "")
                menu.addItemWithTitle("To Number", action: "convertToNumber:", keyEquivalent: "")
            case .BOOL:
                menu.addItemWithTitle("To Null", action: "convertToNull:", keyEquivalent: "")
                menu.addItemWithTitle("To Number", action: "convertToNumber:", keyEquivalent: "")

    default: break

This produces the popup menu:

The example probably speaks for itself, but if not:
  • The switch statement ensures that each case only processes the menu that is assigned to it.
  • In each case it is determined first if the menu is already build or if it has to be build. Note that in the case of the "Convert" menu, the menu is created anew each time since it must be dynamically populated.
  • The first time around, only the Popup menu exists, the other two (sub) menus are created when the Popup menu is selected for the first time.
  • When creating the Add and Convert (sub) menus, the delegate must be set. Setting the delegate ensures that the func menuNeedsUpdate(menu: NSMenu) is called when the user selects it.
  • In the example above, the "Convert" menu item should be invalidated in the func validateMenuItem(menu: NSMenu) when there is no valid object. Otherwise the user may think that the convert menu is available even when it is not.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment