9
votes

Any way to have a NSButton title to wrap when it's width is longer than the button width, instead of getting clipped?

I'm trying to have a radio button with a text that can be long and have multiple lines. One way I thought about having it work is to have an NSButton of type NSRadioButton but can't get multiple lines of text to work.

Maybe my best alternative is to have an NSButton followed by an NSTextView with the mouseDown delegate function on it triggering the NSButton state?

8
Read the Apple Human Interface Guidelines, they should give some insight as to how to deal with the need for descriptive choices.dreamlax

8 Answers

7
votes

I don't believe you can. You'd have to subclass NSButtonCell to add support for this.

That said, it's typically a bad idea to have multiple lines of text on a button. A button label should concisely represent the action performed:

The label on a push button should be a verb or verb phrase that describes the action it performs—Save, Close, Print, Delete, Change Password, and so on. If a push button acts on a single setting, label the button as specifically as possible; “Choose Picture…,” for example, is more helpful than “Choose…” Because buttons initiate an immediate action, it shouldn’t be necessary to use “now” (Scan Now, for example) in the label.

What are you trying to do?

6
votes

I`m incredibly late, but I still feel obliged to share what I`ve found.

Just add a newline character before and after the button title before you assign it to the actual button — and voilà! It now wraps automatically.

The downside of this approach is that, for reasons unknown to me, apps compiled on a certain version of OS X shift button titles one line down when run on newer versions.

4
votes

Well here's my excuse for needing multiline buttons: I'm writing an emulator for an IBM 701, complete with front panel, and, bless their hearts, the designers of that front panel used multi-line labels. Here's my code. You only have to subclass NSButtonCell (not NSButton), and only one method needs to be overridden.

// In Xcode 4.6 (don't know about earlier versions): Place NSButton, then double-click it
// and change class NSButtonCell to ButtonMultiLineCell.

@interface ButtonMultiLineCell : NSButtonCell

@end

@implementation ButtonMultiLineCell

- (NSRect)drawTitle:(NSAttributedString *)title withFrame:(NSRect)frame inView:(NSView *)controlView
{
    NSAttributedString *as = [[NSAttributedString alloc] initWithString:[title.string stringByReplacingOccurrencesOfString:@" " withString:@"\n"]];
    NSFont *sysFont = [NSFont systemFontOfSize:10];
    NSMutableParagraphStyle *paragraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
    [paragraphStyle setAlignment:NSCenterTextAlignment];
    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
      sysFont, NSFontAttributeName,
      paragraphStyle, NSParagraphStyleAttributeName,
      nil];
    NSSize textSize = [as.string sizeWithAttributes:attributes];
    NSRect textBounds = NSMakeRect(0, 0, textSize.width, textSize.height);
    // using frame argument seems to produce text in wrong place
    NSRect f = NSMakeRect(0, (controlView.frame.size.height - textSize.height) / 2, controlView.frame.size.width, textSize.height);
    [as.string drawInRect:f withAttributes:attributes];
    return textBounds; // not sure what rectangle to return or what is done with it
}

@end
2
votes

Even later, but I also feel obliged to share. You can set the attributedTitle property of NSButton to achieve manual wrapping.

In my case, I wanted the button title to wrap if it was greater than 6 characters (Swift 3):

if button.title.characters.count > 6 {
    var wrappedTitle = button.title
    wrappedTitle.insert("\n", at: wrappedTitle.index(wrappedTitle.startIndex, offsetBy: 6))
    let style = NSMutableParagraphStyle()
    style.alignment = .center
    let attributes = [NSFontAttributeName: NSFont.systemFont(ofSize: 19), NSParagraphStyleAttributeName: style] as [String : Any]
    button.attributedTitle = NSAttributedString(string: wrappedTitle, attributes: attributes)
}
1
votes

I'm with Sören; If you need a longer description, think about using a tool tip or placing descriptive text in a wrapped text field using the small system font below the radio choices if the descriptive text is only a few lines. Otherwise, you could provide more information in a help document.

Figuring out a way to say what you need to say in a concise way is your best bet, though.

1
votes

As of today, I'm seeing this can be done simply with a property on the cell of NSButton:

myButton.cell?.wraps = true

0
votes

I had the same problem and tried, with a sinking heart, the solutions in this post. (While I appreciate advice that one generally should keep button titles short, I'm writing a game, and I want multi-line answers to behave like buttons).

Sometimes, you don't get there from here. My ideal was an NSButton with a multi-line label, but since I can't get that without considerable hassle, I have created a PseudoButton: an NSControl subclass that behaves like a button. It has a hand cursor to indicate 'you can click here' and it gives feedback: when you click the mouse, it changes to selectedControlColor, when you release the mouse, it returns to normal. And unlike solutions that try to stack buttons and labels, there is no problem with having labels and images on top of the view: the whole of the view is the clickable area.

import Cocoa

@IBDesignable

class PseudoButton: NSControl {

    @IBInspectable var backgroundColor: NSColor = NSColor.white{
        didSet{
            self.needsDisplay = true
        }
    }

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        let path = NSBezierPath(rect: dirtyRect)
        backgroundColor.setFill()
        path.fill()
        NSColor.black.setStroke()
        path.lineWidth = 2
        path.stroke()
    }


    override func mouseDown(with event: NSEvent) {
        self.backgroundColor = NSColor.selectedControlColor
    }

    override func mouseUp(with event: NSEvent) {
        self.backgroundColor = NSColor.clear
        guard let action = action else {return}
    tryToPerform(action, with: self)
    //@IBAction func pseudobuttonClicked(_ sender: PseudoButton) in the ViewController class
    }

    override func resetCursorRects() {
        addCursorRect(bounds, cursor: .pointingHand)
    }


}

You use this like any other control in the storyboard: drag a Pseudobutton in, decorate it at will, and connect it to an appropriate IBAction in your viewController class.

I like this better than meddling with NSCell. (On past experience, NSCell-based hacks are more likely to break).

0
votes

A little bit late here, here's my code to insert new line in title:

private func calculateMultipleLineTitle(_ title: String) -> String {
    guard !title.isEmpty else { return title }
    guard let cell = cell as? NSButtonCell else { return title }
    let titleRect = cell.titleRect(forBounds: bounds)
    let attr = attributedTitle.attributes(at: 0, effectiveRange: nil)
    let indent = (attr[.paragraphStyle] as? NSMutableParagraphStyle)?.firstLineHeadIndent ?? 0
    let titleTokenArray = title.components(separatedBy: " ")  // word wrap break mode
    guard !titleTokenArray.isEmpty else { return title }
    var multipleLineTitle = titleTokenArray[0]
    var multipleLineAttrTitle = NSMutableAttributedString(string: multipleLineTitle, attributes: attr)
    var index = 1
    while index < titleTokenArray.count {
        multipleLineAttrTitle = NSMutableAttributedString(
            string: multipleLineTitle + " " + titleTokenArray[index],
            attributes: attr
        )
        if titleRect.minX+indent+multipleLineAttrTitle.size().width > bounds.width {
            multipleLineTitle += " \n" + titleTokenArray[index]
        } else {
            multipleLineTitle += " " + titleTokenArray[index]
        }
        index += 1
    }
    return multipleLineTitle
}

Just pass the original title as parameter, it will return multiple line title.