Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 961
cidy.long Avatar asked Apr 27 '19 07:04

cidy.long


2 Answers

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.

like image 151
Ron Avatar answered Nov 02 '22 22:11

Ron


How Toolbars Work

To create a toolbar, you must create a delegate that provides important information:

  • A list of default toolbar identifiers. This list is used when reverting to default, and constructing the initial toolbar. The default set of toolbar items can also be specified using toolbar items found in the Interface Builder library.
  • A list of allowed item identifiers. The allowed item list is used to construct the customization palette, if the toolbar is customizable.
  • The toolbar item for a given item identifier.

For example add a flexibleSpace, print and custom item:

class MyWindowController: NSWindowController, NSToolbarDelegate {

    var toolbarIdentifier = NSToolbarItem.Identifier("FILE_OPEN")

    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.print, toolbarIdentifier]
    }

    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.print, toolbarIdentifier]
    }

    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
        willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
        if itemIdentifier == toolbarIdentifier {
            let toolbarItem = NSToolbarItem(itemIdentifier: toolbarIdentifier)
            toolbarItem.label = String("File")
            toolbarItem.paletteLabel = String("Open File")
            toolbarItem.toolTip = String("Open file to be handled")
            toolbarItem.isEnabled = true
            toolbarItem.target = self
            toolbarItem.action = #selector(browseFile)
            toolbarItem.image = NSImage.init(named:NSImage.folderName)
            return toolbarItem
        }
        else {
            return NSToolbarItem(itemIdentifier: itemIdentifier)
        }
    }

}

It is also possible to add some or all standard and/or custom items in IB.

like image 44
Willeke Avatar answered Nov 02 '22 23:11

Willeke