Brian Postow
Brian Postow

Reputation: 12207

Interface builder segmented controls?

I'm building a toolbar that I want to look at least somewhat like Preview's. I want to create the Previous/Next buttons. The way I THINK it's done is with a segmented control, each has an image in the control. The problem is that I can't figure out how to get the labels underneath. I can get ONE label centered under the whole thing, but labeling the segmented control, But I can't get a Next and a Previous label under each part of the control.

What am I missing here?

thanks.

Upvotes: 3

Views: 3236

Answers (3)

ctietze
ctietze

Reputation: 2932

As @frédéric-blanc pointed out: the only way to achieve this is with NSToolbarItemGroup programmatically.

The trick is to assign both a view and a subitems array to the NSToolbarItemGroup.

let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: "NavigationGroupToolbarItem"))

let itemA = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "PrevToolbarItem"))
itemA.label = "Prev"
let itemB = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "NextToolbarItem"))
itemB.label = "Next"

let segmented = NSSegmentedControl(frame: NSRect(x: 0, y: 0, width: 85, height: 40))
segmented.segmentStyle = .texturedRounded
segmented.trackingMode = .momentary
segmented.segmentCount = 2
// Don't set a label: these would appear inside the button
segmented.setImage(NSImage(named: NSImage.goLeftTemplateName)!, forSegment: 0)
segmented.setWidth(40, forSegment: 0)
segmented.setImage(NSImage(named: NSImage.goRightTemplateName)!, forSegment: 1)
segmented.setWidth(40, forSegment: 1)

// `group.label` would overwrite segment labels
group.paletteLabel = "Navigation"
group.subitems = [itemA, itemB]
group.view = segmented

If you want to dabble with it, see the sample app code below.

Sample App (Updated for Swift 5.1)

Sample window

This is all you need to test this in a new macOS/Cocoa sample app:

  1. In Xcode, create a new macOS project using default options (Swift, storyboards).
  2. Add a new file called WindowController.swift and paste this code into it.
  3. Set the window controller's class in interface builder to WindowController.
import Cocoa

class WindowController: NSWindowController, NSToolbarDelegate {

    var _toolbar: NSToolbar!

    let toolbarItems: [[String: Any]] = [
        ["title" : "irrelevant :)", "icon": "NSPreferencesGeneral", "identifier": NSToolbarItem.Identifier(rawValue: "NavigationGroupToolbarItem")],
        ["title" : "Share", "icon": NSImage.shareTemplateName, "identifier": NSToolbarItem.Identifier(rawValue: "ShareToolbarItem")],
        ["title" : "Add", "icon": NSImage.addTemplateName, "identifier": NSToolbarItem.Identifier(rawValue: "AddToolbarItem")]
    ]

    var toolbarTabsIdentifiers: [NSToolbarItem.Identifier] {

        return toolbarItems.compactMap { $0["identifier"] as? NSToolbarItem.Identifier }

    }

    override func windowDidLoad() {

        _toolbar = NSToolbar(identifier: "TheToolbarIdentifier")
        _toolbar.allowsUserCustomization = true
        _toolbar.delegate = self
        window?.toolbar = _toolbar

    }

    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {

        guard let infoDictionary: [String : Any] = toolbarItems.filter({ $0["identifier"] as? NSToolbarItem.Identifier == itemIdentifier }).first
            else { return nil }

        let toolbarItem: NSToolbarItem

        if itemIdentifier == NSToolbarItem.Identifier(rawValue: "NavigationGroupToolbarItem") {

            let group = NSToolbarItemGroup(itemIdentifier: itemIdentifier)

            let itemA = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "PrevToolbarItem"))
            itemA.label = "Prev"
            let itemB = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "NextToolbarItem"))
            itemB.label = "Next"

            let segmented = NSSegmentedControl(frame: NSRect(x: 0, y: 0, width: 85, height: 40))
            segmented.segmentStyle = .texturedRounded
            segmented.trackingMode = .momentary
            segmented.segmentCount = 2
            // Don't set a label: these would appear inside the button
            segmented.setImage(NSImage(named: NSImage.goLeftTemplateName)!, forSegment: 0)
            segmented.setWidth(40, forSegment: 0)
            segmented.setImage(NSImage(named: NSImage.goRightTemplateName)!, forSegment: 1)
            segmented.setWidth(40, forSegment: 1)

            // `group.label` would overwrite segment labels
            group.paletteLabel = "Navigation"
            group.subitems = [itemA, itemB]
            group.view = segmented

            toolbarItem = group
        } else {
            toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: itemIdentifier.rawValue))
            toolbarItem.label = (infoDictionary["title"] as? String)!

            let iconImage = NSImage(named: (infoDictionary["icon"] as? String)!)
            let button = NSButton(frame: NSRect(x: 0, y: 0, width: 40, height: 40))
            button.title = ""
            button.image = iconImage
            button.bezelStyle = .texturedRounded
            toolbarItem.view = button
        }

        return toolbarItem
    }

    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {

        return self.toolbarTabsIdentifiers

    }

    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {

        return self.toolbarDefaultItemIdentifiers(toolbar)

    }

    func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {

        return self.toolbarDefaultItemIdentifiers(toolbar)

    }

    func toolbarWillAddItem(_ notification: Notification) {

        print("toolbarWillAddItem", (notification.userInfo?["item"] as? NSToolbarItem)?.itemIdentifier ?? "")

    }

    func toolbarDidRemoveItem(_ notification: Notification) {

        print("toolbarDidRemoveItem", (notification.userInfo?["item"] as? NSToolbarItem)?.itemIdentifier ?? "")

    }

}

Upvotes: 6

Brian, you may not have seen my n-th comment (sic) to my initial answer (since Stack Overflow hides them in a collapsable section when there are too many of them). But my last comment answered your latest question (i.e. the one re your inability to build an NSToolbarItemGroup from within Interface Builder).

Now that Snow Leopard is released, I can tell you you're still not able to graphically solve your issue by dragging some control from this newer IB's palette. Nope, your only solution is IMHO, as quoted from my Aug. 27 comment, "to programmatically allocate, initialize, and configure your group before adding it to your toolbar instance."

I hope you'll see this new answer ('cause I do think you simply didn't notice my hidden comment…). Wishing you best of luck, hoping you'll be able to solve your problem with those hints, the needed code still being in the Overview section of the NSToolbarItemGroup class reference.

Upvotes: 1

I'm pretty sure it's a "hack"… In Interface Builder (IB), I've managed to recreate the look you'd like to achieve by using, as mentioned, an NSSegmentedControl instance w/ the "Capsule" style. But for its label, I do think it's "just" the string "Previous Next"… (Yep, w/ some space characters as separators :( !)

EDIT: The right answer can be found in the comments; i.e. from Leopard, one can use the NSToolbarItemGroup class to achieve such a result with a segmented control-based toolbar item, as illustrated by code in the documentation here.

(Sorry for the confusion, but as I'm new to Stack Overflow, I don't know whether it's better to remove my comments—and edit my answer—, or leave them for history purposes…)

Upvotes: 1

Related Questions