Search code examples
swiftmacostoolbaritems

How to add a custom NSToolbarItem to an existing toolbar programmatically


I am having difficult to add a custom NSToolbarItem to my existing toolbar.

NSToolbar was created in NSWindowController, then I have a function to populate toolbar items programmatically, code as:

public func populateFileToolbarItem(_ toolbar: NSToolbar) -> Void{
    let itemId = NSToolbarItem.Identifier("FILE_OPEN")
    let index = toolbar.items.count
    var toolbarItem: NSToolbarItem
    toolbarItem = NSToolbarItem(itemIdentifier: itemId)
    toolbarItem.label = String("File")
    toolbarItem.paletteLabel = String("Open File")
    toolbarItem.toolTip = String("Open file to be handled")
    toolbarItem.tag = index
    toolbarItem.target = self
    toolbarItem.isEnabled = true
    toolbarItem.action = #selector(browseFile)
    toolbarItem.image = NSImage.init(named:NSImage.folderName)
    toolbar.insertItem(withItemIdentifier: itemId, at: index)
}

Then I called this function to add the toolbar item to an existing toolbar in windowController

.......
  populateFileToolbarItem((self.window?.toolbar)!)
  self.window?.toolbar?.insertItem(withItemIdentifier: NSToolbarItem.Identifier.flexibleSpace, at: (self.window?.toolbar?.items.count)!)
  self.window?.toolbar?.insertItem(withItemIdentifier: NSToolbarItem.Identifier.print, at: (self.window?.toolbar?.items.count)!)
  print("after toolbaritems were inserted into toolbar. \(String(describing: self.window?.toolbar?.items.count))") 
......

The console print out shows, there are only two toolbar items were added to toolbar.

.......
after toolbaritems were inserted into toolbar. Optional(2)

And there is no custom item shows in the toolbar.

Any one has experience, please advise!


Solution

  • To add/remove items from the toolbar, you need the toolbar delegate: NSToolbarDelegate.

    Here is a template for the implementation I'm using (probably more than you want).

    Boilerplate code to create toolbar items of various types:

    
    struct ToolbarIdentifiers {
        static let mainToolbar = NSToolbar.Identifier(stringLiteral: "MainToolbar")
        static let navGroupItem = NSToolbarItem.Identifier(rawValue: "NavGroupToolbarItem")
        static let shareItem = NSToolbarItem.Identifier(rawValue: "ShareToolBarItem")
        static let addItem = NSToolbarItem.Identifier(rawValue: "AddToolbarItem")
        static let statusItem = NSToolbarItem.Identifier(rawValue: "StatusToolbarItem")
        static let filterItem = NSToolbarItem.Identifier(rawValue: "FilterToolbarItem")
        static let sortItem = NSToolbarItem.Identifier(rawValue: "SortToolbarItem")
        static let cloudUploadItem = NSToolbarItem.Identifier(rawValue: "UploadToolbarItem")
        static let cloudDownloadItem = NSToolbarItem.Identifier(rawValue: "DownloadToolbarItem")
        static let leftButtonItem = NSToolbarItem.Identifier(rawValue: "leftButtonToolbarItem")
        static let rightButtonItem = NSToolbarItem.Identifier(rawValue: "rightButtonToolbarItem")
        static let hideShowItem = NSToolbarItem.Identifier(rawValue: "hideShowToolbarItem")
    }
    
    // Base toolbar item type, extended for segmented controls, buttons, etc.
    struct ToolbarItem {
        let identifier: NSToolbarItem.Identifier
        let label: String
        let paletteLabel: String
        let tag: ToolbarTag
        let image: NSImage?
        let width: CGFloat
        let height: CGFloat
        let action: Selector?
        weak var target: AnyObject?
        var menuItem: NSMenuItem? = nil // Needs to be plugged in after App has launched.
        let group: [ToolbarItem]
    
        init(_ identifier: NSToolbarItem.Identifier, label: String = "", tag: ToolbarTag = .separator, image: NSImage? = nil,
             width: CGFloat = 38.0, height: CGFloat = 28.0,
             action: Selector? = nil, target: AnyObject? = nil, group: [ToolbarItem] = [], paletteLabel: String = "") {
            self.identifier = identifier
            self.label = label
            self.paletteLabel = paletteLabel
            self.tag = tag
            self.width = width
            self.height = height
            self.image = image
            self.action = action
            self.target = target
            self.group = group
        }
    }
    // Image button -- creates NSToolbarItem
    extension ToolbarItem {
        func imageButton() -> NSToolbarItem {
            let item = NSToolbarItem(itemIdentifier: identifier)
            item.label = label
            item.paletteLabel = label
            item.menuFormRepresentation = menuItem // Need this for text-only to work
            item.tag = tag.rawValue
            let button = NSButton(image: image!, target: target, action: action)
            button.widthAnchor.constraint(equalToConstant: width).isActive = true
            button.heightAnchor.constraint(equalToConstant: height).isActive = true
            button.title = ""
            button.imageScaling = .scaleProportionallyDown
            button.bezelStyle = .texturedRounded
            button.tag = tag.rawValue
            button.focusRingType = .none
            item.view = button
            return item
        }
    }
    // Segmented control -- creates NSToolbarItemGroup containing multiple instances of NSToolbarItem
    extension ToolbarItem {
        func segmentedControl() -> NSToolbarItemGroup {
            let itemGroup = NSToolbarItemGroup(itemIdentifier: identifier)
            let control = NSSegmentedControl(frame: NSRect(x: 0, y: 0, width: width, height: height))
            control.segmentStyle = .texturedSquare
            control.trackingMode = .momentary
            control.segmentCount = group.count
            control.focusRingType = .none
            control.tag = tag.rawValue
    
            var items = [NSToolbarItem]()
            var iSeg = 0
            for segment in group {
                let item = NSToolbarItem(itemIdentifier: segment.identifier)
                items.append(item)
                item.label = segment.label
                item.tag = segment.tag.rawValue
                item.action = action
                item.target = target
                control.action = segment.action // button & container send to separate handlers
                control.target = segment.target
                control.setImage(segment.image, forSegment: iSeg)
                control.setImageScaling(.scaleProportionallyDown, forSegment: iSeg)
                control.setWidth(segment.width, forSegment: iSeg)
                control.setTag(segment.tag.rawValue, forSegment: iSeg)
                iSeg += 1
            }
            itemGroup.paletteLabel = paletteLabel
            itemGroup.subitems = items
            itemGroup.view = control
            return itemGroup
        }
    }
    // Text field -- creates NSToolbarItem containing NSTextField
    extension ToolbarItem {
        func textfieldItem() -> NSToolbarItem {
            let item = NSToolbarItem(itemIdentifier: identifier)
            item.label = ""
            item.paletteLabel = label
            item.tag = tag.rawValue
            let field = NSTextField(string: label)
            field.widthAnchor.constraint(equalToConstant: width).isActive = true
            field.heightAnchor.constraint(equalToConstant: height).isActive = true
            field.tag = tag.rawValue
            field.isSelectable = false
            item.view = field
            return item
        }
    }
    // Menu item -- creates an empty NSMenuItem so that user can click on the label
    // definitely a work-around till we implement the menus
    extension ToolbarItem {
        mutating func createMenuItem(_ action: Selector) {
            let item = NSMenuItem()
            item.action = action
            item.target = target
            item.title = label
            item.tag = tag.rawValue
            self.menuItem = item
        }
    }
    /*
     * Create specialized toolbar items with graphics, labels, actions, etc
     * Encapsulates implementation-specific details in code, because the table-driven version was hard to read.
     */
    struct InitializeToolbar {
    }
    extension InitializeToolbar {
        static func navGroupItem(_ action: Selector, segmentAction: Selector, target: AnyObject) -> ToolbarItem {
            var group = [ToolbarItem]()
            group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "BackToolbarItem"), label: "Prev", tag: .navPrev,
                                     image: NSImage(named: NSImage.goBackTemplateName), action: segmentAction, target: target))
            group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "FwdToolbarItem"), label: "Next", tag: .navNext,
                                     image: NSImage(named: NSImage.goForwardTemplateName), action: segmentAction, target: target))
            let item = ToolbarItem(ToolbarIdentifiers.navGroupItem, tag: .navGroup, width: 85, height: 28,
                                   action: action, target: target, group: group, paletteLabel: "Navigation")
            return item
        }
    }
    extension InitializeToolbar {
        static func hideShowItem(_ action: Selector, segmentAction: Selector, target: AnyObject) -> ToolbarItem {
            var group = [ToolbarItem]()
            group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "HideLeftItem"), label: "", tag: .leftButton,
                                     image: NSImage(named: "leftButton"), action: segmentAction, target: target))
            group.append(ToolbarItem(NSToolbarItem.Identifier(rawValue: "HideRightItem"), label: "", tag: .rightButton,
                                     image: NSImage(named: "rightButton"), action: segmentAction, target: target))
            let item = ToolbarItem(ToolbarIdentifiers.hideShowItem, tag: .hideShow, width: 85, height: 28,
                                   action: action, target: target, group: group, paletteLabel: "Hide/Show")
            return item
        }
    }
    extension InitializeToolbar {
        static func addItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.addItem, label: "Add", tag: .add, image: NSImage(named: NSImage.addTemplateName), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func shareItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.shareItem, label: "Share", tag: .share, image: NSImage(named: NSImage.shareTemplateName), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func filterItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.filterItem, label: "Filter", tag: .filter, image: NSImage(named: "filter"), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func sortItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.sortItem, label: "Sort", tag: .sort, image: NSImage(named: "sort"), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func cloudDownloadItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.cloudDownloadItem, label: "Down", tag: .cloudDownload, image: NSImage(named: "cloudDownload"), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func cloudUploadItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.cloudUploadItem, label: "Up", tag: .cloudUpload, image: NSImage(named: "cloudUpload"), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func leftButtonItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.leftButtonItem, label: "", tag: .leftButton, image: NSImage(named: "leftButton"), action: action, target: target)
            return item
        }
    }
    extension InitializeToolbar {
        static func rightButtonItem(_ action: Selector, target: AnyObject) -> ToolbarItem {
            let item = ToolbarItem(ToolbarIdentifiers.rightButtonItem, label: "", tag: .rightButton, image: NSImage(named: "rightButton"), action: action, target: target)
            return item
        }
    }
    
    extension InitializeToolbar {
        static func textItem() -> ToolbarItem {
            return ToolbarItem(ToolbarIdentifiers.statusItem, label: "Watch This Space", tag: .status, width: 300, height: 24)
        }
    }
    

    Here is the toolbar class, which implements the initializer and delegate:

    /*
     * Initializer builds a specialized toolbar.
     */
    enum ToolbarTag: Int {
        case separator = 1
        case navGroup
        case navPrev
        case navNext
        case add
        case share
        case filter
        case sort
        case cloudDownload
        case cloudUpload
        case leftButton
        case rightButton
        case hideShow
        case status
    }
    class Toolbar: NSObject, NSToolbarDelegate, Actor {
        var actorDelegate: ActorDelegate?
        var identifier: NSUserInterfaceItemIdentifier?
        var toolbarItemList = [ToolbarItem]()
        var toolbarItemIdentifiers: [NSToolbarItem.Identifier] { return toolbarItemList.map({ $0.identifier }) }
        var toolbarDefaultItemList = [ToolbarItem]()
        var toolbarDefaultItemIdentifiers: [NSToolbarItem.Identifier] { return toolbarDefaultItemList.map({ $0.identifier }) }
    
        // Delegate toolbar actions
        @objc func controlSentAction(_ sender: Any) {
            guard let control = sender as? NSControl else { return }
            guard let tag = ToolbarTag(rawValue: control.tag) else { return }
            actorDelegate?.actor(self, initiator: control, tag: tag, obj: nil)
        }
        @objc func segmentedControlSentAction(_ sender: Any) {
            guard let segmented = sender as? NSSegmentedControl else { return }
            guard let tag = ToolbarTag(rawValue: segmented.tag(forSegment: segmented.selectedSegment)) else { return }
            actorDelegate?.actor(self, initiator: segmented, tag: tag, obj: nil)
        }
        // These don't get called at the moment
        @objc func toolbarItemSentAction(_ sender: Any) { ddt("toolbarItemSentAction") }
        @objc func menuSentAction(_ sender: Any) { ddt("menuSentAction") }
    
        // Toolbar initialize
        init(_ window: Window) {
            super.init()
            identifier = Identifier.View.toolbar
    
            let toolbar = NSToolbar(identifier: ToolbarIdentifiers.mainToolbar)
            toolbar.centeredItemIdentifier = ToolbarIdentifiers.statusItem
    
            // Build the initial toolbar
            // Text field
            toolbarItemList.append(ToolbarItem(.flexibleSpace))
            toolbarItemList.append(InitializeToolbar.textItem())
            toolbarItemList.append(ToolbarItem(.flexibleSpace))
            // Show/Hide
            toolbarItemList.append(InitializeToolbar.hideShowItem(#selector(toolbarItemSentAction), segmentAction: #selector(segmentedControlSentAction), target: self))
            // Save initial toolbar as default
            toolbarDefaultItemList = toolbarItemList
            // Also allow these, just to demo adding
            toolbarItemList.append(InitializeToolbar.cloudDownloadItem(#selector(controlSentAction), target: self))
            toolbarItemList.append(InitializeToolbar.sortItem(#selector(controlSentAction), target: self))
    
            toolbar.allowsUserCustomization = true
            toolbar.displayMode = .default
            toolbar.delegate = self
            window.toolbar = toolbar
        }
    
        deinit {
            ddt("deinit", caller: self)
        }
    }
    /*
     * Implement NSToolbarDelegate
     */
    extension Toolbar {
    
        // Build toolbar
        func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
            guard let item = toolbarItemList.firstIndex(where: { $0.identifier == itemIdentifier }) else { return nil }
            switch toolbarItemList[item].identifier {
            case ToolbarIdentifiers.navGroupItem, ToolbarIdentifiers.hideShowItem:
                return toolbarItemList[item].segmentedControl()
            case ToolbarIdentifiers.addItem, ToolbarIdentifiers.shareItem, ToolbarIdentifiers.sortItem, ToolbarIdentifiers.filterItem, ToolbarIdentifiers.cloudUploadItem, ToolbarIdentifiers.cloudDownloadItem,
                 ToolbarIdentifiers.leftButtonItem, ToolbarIdentifiers.rightButtonItem:
                return toolbarItemList[item].imageButton()
            case ToolbarIdentifiers.statusItem:
                return toolbarItemList[item].textfieldItem()
            default:
                return nil
            }
        } // end of toolbar
    
        func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
            return toolbarDefaultItemIdentifiers;
        }
    
        func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
            return toolbarItemIdentifiers
        }
    
        func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
            return []
        }
    
        func toolbarWillAddItem(_ notification: Notification) {
        }
    
        func toolbarDidRemoveItem(_ notification: Notification) {
        }
    } // End of extension
    

    Initial Toolbar:

    enter image description here

    Customization drop-down, which Cocoa does for you:

    enter image description here

    After adding cloud button:

    enter image description here Hope this is helpful.

    Added to clarify 4/28/2019:

    My Toolbar class is not an NSToolbar subclass. Its initializer gets passed a reference to the window, so that at the end it sets the window's toolbar to the toolbar it creates:

        init(_ window: Window) {
            super.init()
            identifier = Identifier.View.toolbar
    
    **** stuff removed for clarity ****
    
            let toolbar = NSToolbar(identifier: ToolbarIdentifiers.mainToolbar)
            toolbar.allowsUserCustomization = true
            toolbar.displayMode = .default
            toolbar.delegate = self
            window.toolbar = toolbar
        }
    
    

    Perhaps this is confusing semantics, but it creates the toolbar and acts as the toolbar delegate, as you can see in the extension.

    The "Actor" protocol is part of my coordination framework, not important to constructing the toolbar itself. I would have had to include the entire demo app to show that, and I assume that you have your own design for passing toolbar actions to your controllers/models.

    This app is Xcode 10.2/Swift 5, although I don't think it uses any new Swift 5 features.