How to customize disclosure cell in view-based NSOutlineView
Asked Answered
F

4

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.

Fiscus answered 20/6, 2012 at 20:35 Comment(0)
M
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.

Micelle answered 21/6, 2012 at 8:23 Comment(5)
Thanks for reply. So the outlineView:rowViewForItem basically will replace the outlineView:viewForTableColumn in case of expandable cells?Fiscus
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.Micelle
I'll try it and let you know.Thanks a lot!Fiscus
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.Fiscus
don't forget to call [super didAddSubview:] -- otherwise, your going to have some trouble...Ballman
U
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"
Uredo answered 8/12, 2013 at 14:17 Comment(4)
What happens where you have //Do your customization?Bayard
@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.Uredo
e.g. [(NSButton *)view setImage:[NSImage imageNamed:@"disclosure-closed"]];Uredo
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.Scuppernong
M
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.

Micelle answered 21/6, 2012 at 8:23 Comment(5)
Thanks for reply. So the outlineView:rowViewForItem basically will replace the outlineView:viewForTableColumn in case of expandable cells?Fiscus
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.Micelle
I'll try it and let you know.Thanks a lot!Fiscus
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.Fiscus
don't forget to call [super didAddSubview:] -- otherwise, your going to have some trouble...Ballman
T
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!

Ted answered 1/7, 2018 at 7:42 Comment(1)
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?Robbynrobe
S
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")
    }
}
Sheepdog answered 5/9, 2016 at 12:52 Comment(2)
Thanks - now we just need a Swift version of WetFish's answer!Micelle
You're a gangster, Thanks! New method name: func didAddSubview(_ subview: NSView)Fiche

© 2022 - 2024 — McMap. All rights reserved.