9

I'm subclassing NSButtonCell to customize the drawing (customizable theme). I'd like to customize the way checkboxes and radio buttons are drawn.

Does anyone know how to detect whether a button is a checkbox or radio button?

There is only -setButtonType:, no getter, and neither -showsStateBy nor -highlightsBy seem to give any unique return values for checkboxes that don't also apply to regular push buttons with images and alternate images.

So far I've found two (not very pretty) workarounds, but they're the kind of thing that'd probably get the app rejected from MAS:

  1. Use [self valueForKey: @"buttonType"]. This works, but since the method is not in the headers, I presume this is something Apple wouldn't want me to do.

  2. Override -setButtonType: and -initWithCoder: to keep track of the button type when it is set manually or from the XIB. Trouble here is the XIB case, because the keys used to save the button type to disk are undocumented. So again, I'd be using private API.

I'd really like this to be a straight drop-in replacement for NSButtonCell instead of forcing client code to use a separate ULIThemeSwitchButtonCell class for checkboxes and a third one for radio buttons.

uliwitness
  • 8,532
  • 36
  • 58
  • 1
    For what it's worth, `[self valueForKey: @"buttonType"]` doesn't work on OS 10.7, it throws an undefined key exception. – JWWalker Nov 15 '16 at 23:02

4 Answers4

2

A button does not know anything about its style.

From the documentation on NSButton

Note that there is no -buttonType method. The set method sets various button properties that together establish the behavior of the type. -

You could use tag: and setTag: (inherited by NSButton from NSControl) in order to mark the button either as a checkbox or a radio button. If you do that programatically then you should define the constant you use. You can also set the tag in Interface Builder, but only as an integer value (magic number).

Pétur Ingi Egilsson
  • 4,368
  • 5
  • 44
  • 72
  • So I guess then the question would be: What combination of properties are set that I would have to look for to uniquely identify I'm currently a checkbox/radio button. Particularly the checkbox/radio button graphics would be helpful, as that is what I want to replace with my themed graphics. – uliwitness Oct 27 '13 at 12:52
  • @Matt I guess so. At least, -setButtonType: is the only officially exposed method so far. It wasn't made a property. – uliwitness Oct 10 '16 at 17:31
0

In initWithCoder, here is my adaptation of the BGHUDButtonCell.m solution, updated for Mac OS Sierra:

-(id)initWithCoder:(NSCoder *)aDecoder {

   if ( !(self = [super initWithCoder: aDecoder]) ) return nil;

   NSImage *normalImage = [aDecoder decodeObjectForKey:@"NSNormalImage"];
   if ( [normalImage isKindOfClass:[NSImage class]] )
   {
      DLog( @"buttonname %@", [normalImage name] );
      if ( [[normalImage name] isEqualToString:@"NSSwitch"] )
         bgButtonType = kBGButtonTypeSwitch;
      else if ( [[normalImage name] isEqualToString:@"NSRadioButton"] )
         bgButtonType = kBGButtonTypeRadio;
   }
   else
   {
      // Mac OS Sierra update (description has word "checkbox")
      NSImage *img = [self image];
      if ( img && [[img description] rangeOfString:@"checkbox"].length )
      {
         bgButtonType = kBGButtonTypeSwitch;
      }
   }
}
Keith Knauber
  • 752
  • 6
  • 13
0

This is strange to me that it's missing from NSButton. I don't get it. That said, it's easy enough to extend NSButton to store the last set value:

import Cocoa

public class TypedButton: NSButton {
    private var _buttonType: NSButton.ButtonType = .momentaryLight
    public var buttonType: NSButton.ButtonType {
        return _buttonType
    }

    override public func setButtonType(_ type: NSButton.ButtonType) {
        super.setButtonType(type)
        _buttonType = type
    }
}
Ryan Francesconi
  • 988
  • 7
  • 17
  • If initWithCoder calls setButtonType this would work. Not sure if it gets called. – Keith Knauber Jan 09 '19 at 01:05
  • yes, you'd need to call it yourself to store it in this case. this is just a simple example. – Ryan Francesconi Jan 09 '19 at 21:27
  • Have you tried whether that works with XIBs? Last I tried, XIBs did not call through setButtonType, so this solution didn't work. You'd need to override init(coder:) to also get the value from there. – uliwitness Jan 10 '19 at 09:21
  • i don't think the value is actually visible in any way?, so as far as I can tell it still requires explicitly setting it yourself after instantiation. That still seems safer to me than parsing description strings though. Still rather baffled as to why this is like this. – Ryan Francesconi Jan 10 '19 at 17:39
  • Because Apple doesn’t want people to make their own themes of Aqua. – Keith Knauber Jan 14 '19 at 01:38
0

Swift 5.5

This is my approach. I use a standard naming convention in my app that relies on plain language identifiers. All my UI elements incorporate their respective property names and what type of UI element is associated with the property. It can make for some pretty long IBOutlet and IBAction names, but remembering tag numbers is way too complicated for me.

For example:

@IBOutlet weak var serveBeerCheckbox: NSButton!
@IBOutlet weak var headSize0RadioButton: NSButton!
@IBOutlet weak var headSize1RadioButton: NSButton!
@IBOutlet weak var headSize2RadioButton: NSButton!
\\ etc.

If there are UI properties that need to be stored, I name those without the type of UI element:

var serveBeer: Bool = true
var headSize: Int = 1

Bare bones example:

import Cocoa

class ViewController: NSViewController {
    
    @IBOutlet weak var serveBeerCheckbox: NSButton!
    @IBOutlet weak var headSize0RadioButton: NSButton!
    @IBOutlet weak var headSize1RadioButton: NSButton!
    @IBOutlet weak var headSize2RadioButton: NSButton!

    var serveBeer: Bool = true
    var headSize: Int = 1

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    @IBAction func buttonClicked(button: NSButton) {
        guard let identifier = button.identifier else { return }
        if identifier.rawValue.contains("Checkbox") {
            switch button.identifier {
                case serveBeerCheckbox.identifier:
                // Do something with the Checkbox
                serveBeer = (serveBeerCheckbox?.state == .on)
                default:
                    // Another checkbox button
            }
        } else if identifier.rawValue.contains("RadioButton") {
            switch button.identifier {
                case headSize0RadioButton.identifier:
                    headSize = 0
                case headSize1RadioButton.identifier:
                    headSize = 1
                case headSize2RadioButton.identifier:
                    headSize = 2
                default:
            }
        } // You could continue checking for different types of buttons
        print("Serve beer? \(serveBeer ? "Sure!" : "Sorry, no.")")
        if serveBeer {
            switch headSize {
                case 1:
                    print("With one inch of head.")
                case 2:
                    print("With two inches of head!")
                default:
                    print("Sorry, no head with your beer.")
            }
        }
    }
}

As you can see, one could write a very generic method that can work on any type of UI element and use the rawValue of the identifier string with .contains() to isolate the type of element being worked with.

I have found using this approach allows me to initialize a UI with a lot of different elements pretty quickly and efficiently without having to recall tag numbers.

SouthernYankee65
  • 1,129
  • 10
  • 22
  • Your solution is to a different problem. And the correct way to do what you're doing is to simply give every button type a different action method, instead of funneling them all through a single buttonClicked method. – uliwitness Mar 31 '22 at 10:23
  • @uliwitness, doesn't that defeat the purpose of reusable code? – SouthernYankee65 Oct 13 '22 at 15:17
  • Code is only reusable if it does the same thing. Trying to funnel unrelated things through the same code and reinventing method dispatch by using conditionals (when using different methods would have made the conditionals unnecessary) is not code reuse. You can always create a -serveBeer method for the common code, and create separate -checkboxClicked: and -radioButtonClicked: methods for the checkbox and radio button conditions instead. – uliwitness Oct 16 '22 at 11:37
  • Better name than `-serveBeer` would probably be `-printServeBeer` or so. Whatever the common code does in your real app. – uliwitness Oct 16 '22 at 11:43