16

I'm trying to customize the disclosure arrow appearance in my view-based NSOutlineView. I saw that it's recommended to use

- (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item

delegate method to achieve it. The problem is that this method is not called for some reason. I have 2 custom cell views - one for item and second for header item. May be this method is not called for view-based outline views? May be something became broken in Lion?

Please shed some light.

Monolo
  • 18,205
  • 17
  • 69
  • 103
Nava Carmon
  • 4,523
  • 3
  • 40
  • 74

4 Answers4

33

Solution 1:

Subclass NSOutlineView and override makeViewWithIdentifier:owner:

- (id)makeViewWithIdentifier:(NSString *)identifier owner:(id)owner {
    id view = [super makeViewWithIdentifier:identifier owner:owner];

    if ([identifier isEqualToString:NSOutlineViewDisclosureButtonKey]) {
        // Do your customization
    }

    return view;
}

For Source Lists use NSOutlineViewShowHideButtonKey.

Solution 2:

Interface Builder

The button is added to the column and the identifier set to NSOutlineViewDisclosureButtonKey.

enter image description here

Official documentation from NSOutlineView.h

/* The following NSOutlineView*Keys are used by the View Based NSOutlineView to create the "disclosure button" used to collapse and expand items. The NSOutlineView creates these buttons by calling [self makeViewWithIdentifier:owner:] passing in the key as the identifier and the delegate as the owner. Custom NSButtons (or subclasses thereof) can be provided for NSOutlineView to use in the following two ways:
 1. makeViewWithIdentifier:owner: can be overridden, and if the identifier is (for instance) NSOutlineViewDisclosureButtonKey, a custom NSButton can be configured and returned. Be sure to set the button.identifier to be NSOutlineViewDisclosureButtonKey.
 2. At design time, a button can be added to the outlineview which has this identifier, and it will be unarchived and used as needed.
 
 When a custom button is used, it is important to properly set up the target/action to do something (probably expand or collapse the rowForView: that the sender is located in). Or, one can call super to get the default button, and copy its target/action to get the normal default behavior.
 
 NOTE: These keys are backwards compatible to 10.7, however, the symbol is not exported prior to 10.9 and the regular string value must be used (i.e.: @"NSOutlineViewDisclosureButtonKey").
 */
APPKIT_EXTERN NSString *const NSOutlineViewDisclosureButtonKey NS_AVAILABLE_MAC(10_9); // The normal triangle disclosure button
APPKIT_EXTERN NSString *const NSOutlineViewShowHideButtonKey NS_AVAILABLE_MAC(10_9); // The show/hide button used in "Source Lists"
Community
  • 1
  • 1
WetFish
  • 1,172
  • 2
  • 11
  • 21
  • What happens where you have `//Do your customization`? – Clifton Labrum Oct 20 '17 at 17:27
  • @CliftonLabrum That's up to you. You can modify `view` (NSButton created by superclass) or create and return your own view. But be sure to set the identifier if you create your own view. – WetFish Oct 20 '17 at 21:43
  • 1
    e.g. `[(NSButton *)view setImage:[NSImage imageNamed:@"disclosure-closed"]];` – WetFish Oct 20 '17 at 21:48
  • This is helpful but I only want to customize the disclosure button for the first row. How can I determine the row or item for the view being requested as this request is not generated by the delegate's `outlineView:viewForTableColumn:item:` method but by the outlineView's private `_updateDisclosureButtonForRowView:forRow:removeIfNotAvailable:updatePosition:inDidAddRowView:` method. – Jon Mar 22 '18 at 21:11
12

This answer is written with OS X 10.7 in mind, for newer versions of OS X/macOS, refer to WetFish's answer

That method does not get called because it is only relevant for cell based outline views.

In a view based outline view, the disclosure triangle is a regular button in the row view of expandable rows. I don't know where it gets added, but it does, and NSView's didAddSubview: method handles exactly that situation of a view being added somewhere else.

Hence, subclass NSTableRowView, and override didAddSubview:, like this:

-(void)didAddSubview:(NSView *)subview
{
    // As noted in the comments, don't forget to call super:
    [super didAddSubview:subview];

    if ( [subview isKindOfClass:[NSButton class]] ) {
        // This is (presumably) the button holding the 
        // outline triangle button.
        // We set our own images here.
        [(NSButton *)subview setImage:[NSImage imageNamed:@"disclosure-closed"]];
        [(NSButton *)subview setAlternateImage:[NSImage imageNamed:@"disclosure-open"]];
    }
}

Of course, your outline view's delegate will have to implement outlineView:rowViewForItem: to return the new row view.

Despite the name, frameOfOutlineCellAtRow: of NSOutlineView still gets called for view based outline views, so for the positioning of your triangle, you might want to subclass the outline view and override that method, too.

Community
  • 1
  • 1
Monolo
  • 18,205
  • 17
  • 69
  • 103
  • Thanks for reply. So the outlineView:rowViewForItem basically will replace the outlineView:viewForTableColumn in case of expandable cells? – Nava Carmon Jun 21 '12 at 08:30
  • I guess it depends how you want to do it. I use both, the row view to draw the background (and the disclosure triangle, as it happens) and the cell view for the contents. I believe that is also how it is intended, but views are flexible by nature. – Monolo Jun 21 '12 at 08:34
  • I'll try it and let you know.Thanks a lot! – Nava Carmon Jun 21 '12 at 08:36
  • Thank you very much! On the apple list they suggested to fill a bug on both - absence of proper customizing interface and poor documentation on this. – Nava Carmon Jun 23 '12 at 20:51
  • 1
    don't forget to call [super didAddSubview:] -- otherwise, your going to have some trouble... – corbin dunn Jun 25 '12 at 20:32
8

For Swift 4.2 macOS 10.14, @WetFish's answer can be implemented as follows:

class SidebarView: NSOutlineView {

  override func makeView(withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?) -> NSView? {
    let view = super.makeView(withIdentifier: identifier, owner: owner)

    if identifier == NSOutlineView.disclosureButtonIdentifier {
      if let btnView = view as? NSButton {
        btnView.image = NSImage(named: "RightArrow")
        btnView.alternateImage = NSImage(named: "DownArrow")

        // can set properties of the image like the size
        btnView.image?.size = NSSize(width: 15.0, height: 15.0)
        btnView.alternateImage?.size = NSSize(width: 15.0, height: 15.0)
      }
    }
    return view
  }

}

Looks quite nice!

Luka Kerr
  • 4,161
  • 7
  • 39
  • 50
  • I followed your answer and I was able to add custom separator. But I am getting a separator line between each group. How do I remove that? – prabhu Jan 22 '20 at 09:31
2

Swift2 version of @Monolo's answer:

override func didAddSubview(subview: NSView) {
    super.didAddSubview(subview)
    if let sv = subview as? NSButton {
        sv.image = NSImage(named:"icnArwRight")
        sv.alternateImage = NSImage(named:"icnArwDown")
    }
}
emreoktem
  • 2,409
  • 20
  • 36