Getting clicked item index in NSPathControl with pathItems instead of pathComponentCells

Since NSPathControl.setPathComponentCells(_:) and .clickedPathComponentCell() are deprecated, I'm trying to use pathItems and clickedPathItem instead. Since I'm representing a virtual path, I cannot use the NSPathControl.url setter, but instead set pathItems directly.

The problem is that in the action method it doesn't seem possible to get the index of the clicked path item, nor does it seem possible to associate any kind of data with each path item, since when the action method is called, the actual object instances stored in pathItems and also the one returned by clickedPathItem change every time.

Here is the sample code that reproduces the issue:

class ViewController: NSViewController {

    @IBOutlet weak var pathControl: NSPathControl!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        pathControl.pathItems = ["a", "b", "c"].map({ title in
            let item = NSPathControlItem()
            item.title = title
            return item
        })
    }

    @IBAction func selectPath(_ sender: NSPathControl) {
        print(sender.clickedPathItem!.description, sender.clickedPathItem!.title, sender.pathItems.description)
    }
    
}

Here is a sample output (notice how the printed addresses change every time):

<NSPathControlItem: 0x6000012780a0> a [<NSPathControlItem: 0x6000012780a0>, <NSPathControlItem: 0x600001278020>, <NSPathControlItem: 0x600001278090>]
<NSPathControlItem: 0x600001278070> a [<NSPathControlItem: 0x600001278070>, <NSPathControlItem: 0x600001278140>, <NSPathControlItem: 0x6000012780d0>]
<NSPathControlItem: 0x60000124c030> a [<NSPathControlItem: 0x60000124c030>, <NSPathControlItem: 0x60000124c080>, <NSPathControlItem: 0x60000124c070>]

it doesn't seem possible to get the index of the clicked path item, nor does it seem possible to associate any kind of data with each path item,

What happens when you try this:

-(void)pathControlAction:(NSPathControl*)pathControl

{
    NSPathControlItem *clickedPathItem = pathControl.clickedPathItem;

    if (clickedPathItem == nil) { NSLog(@"whoops!"); return; }

    NSUInteger index = [pathControl.pathItems indexOfObject:clickedPathItem];

    if (index == NSNotFound) { NSLog(@"whoops!"); return;  }

    //do whatever.

}

They deprecated the cell based NSPathControl API without introducing proper replacements as you discovered. It's been so many years I get the feeling that they are never going to improve this so it's probably pretty safe to use the cell based stuff even though it's deprecated.

But if you don't want to do that your probably can associate data with path control items by subclassing and setting the path control items with some custom objects at the same time.

@interface MyPathControl : NSPathControl

//Each array must contain the same number of objects.
-(void)setPathControlItems:(NSArray<NSPathControlItem*>*)itemsArray 
    withRepresentedObjects:(NSArray<WhateverObject*>*)representedObjectsForEachItem;

-(void)setPathControlItems:(NSArray<NSPathControlItem*>*)itemsArray NS_UNAVAILABLE;

@end

Or something like that. Not great but that's all I can think of.

The documentation in the header file for NSPathControlItem also states: "NSPathControlItem should not be subclassed." I tried doing so nonetheless, but sender.clickedPathItem as! MyPathControlItem fails with the error Could not cast value of type 'NSPathControlItem' (0x20ae78870) to 'MyPathControlItem' (0x1005c0688). So it seems like any custom subclass is replaced again with NSPathControlItem.

That's the incredible thing: sender.pathItems.firstIndex(of: sender.clickedPathItem!) always returns nil, even if the clicked path item has the same address as the corresponding item in the path items array. Even sender.pathItems.firstIndex(where: { $0.description == sender.clickedPathItem!.description }) returns nil. And when executing that print statement more than once one after the other, each item always has a different address.

Not sure. I don't have time to test this thoroughly but try overriding the pathControlItems setter to see if you or the system are resetting the array unexpectedly. How are you creating each NSPathControlItem?

In any case as long as each NSPathControlItem URL is unique you can just hold whatever data you want to associate with each path item like so:

@property NSDictionary<NSURL*,Whatever*>*representedObjectsForPathURLs;

Then add a method on your path control subclass:

-(Whatever*)representedObjectForPathControlItem:(NSPathControlItem*)item
{
    return [self.representedObjectsForPathURLs objectForKey:item.URL];
}

This assumes NSPathControlItem doesn't mangle your url strings after they are set.

Find below my sample code that shows how it's impossible to associate any custom data with a path item. PathControl.pathItems.didSet is called only once when explicitly setting them in loadView(), and in the action selectPath(_:) the path items are not of my custom subclass PathControlItem but are again NSPathControlItem. They change their address on every click, and finding the clicked path item in the pathItems array always returns nil even if by comparing the logged addresses it appears to be there.

class ViewController: NSViewController {

    override func loadView() {
        let pathControl = PathControl()
        pathControl.action = #selector(selectPath(_:))
        pathControl.pathItems = ["a", "b", "c"].map({ title in
            let item = PathControlItem()
            item.title = title
            item.customData = title
            return item
        })
        view = NSStackView(views: [pathControl])
    }

    @IBAction func selectPath(_ sender: NSPathControl) {
        print("click", sender.clickedPathItem!.description, (sender.clickedPathItem as? PathControlItem)?.customData, sender.pathItems.description, sender.pathItems.firstIndex(of: sender.clickedPathItem!), sender.pathItems.firstIndex(where: { $0.description == sender.clickedPathItem!.description }))
    }
    
}

class PathControl: NSPathControl {
    
    override var pathItems: [NSPathControlItem] {
        didSet {
            print("didSet", pathItems)
        }
    }
    
}

class PathControlItem: NSPathControlItem {
    
    var customData = ""
    
}

Take out your PathControlItem subclass. Add a property on PathControl that maps the the path control item titles to your custom data (assuming each path control item has a unique title). Just change the code from my previous response to use the title as the dictionary key instead of NSURL if you aren't setting a URL.

//Add this to PathControl.

@interface PathControl : NSPathControl
//Set this.
@property NSDictionary<NSString*,Whatever*>*representedObjectsForPathItemTitles; 

-(Whatever*)representedObjectForPathControlItem:(NSPathControlItem*)item;

@end

@implementation PathControl

-(Whatever*)representedObjectForPathControlItem:(NSPathControlItem*)item
{
    return [self.representedObjectsForPathItemTitles objectForKey:item.title];
}

Unless your titles aren't unique, in which case you'd have to figure out another way to do this.

Yes. Path titles for files may not be unique, so that is a problem. I've been ******** about NSPathControl for several years and recently filed another bug report which has not even been acknowledged. This thing has never worked with "items", but they refuse to do anything about it, so I'm still using cells, which do work. ClickedPathItem will never return an item in the items array. Also, I need the geometry of each cell, which I use for hover popovers, and you cannot obtain that using items.

Ya I'd just keep using the cell based API and suppress the deprecated warning then...and just file a bug (or wait if you already filed it).

Good news is if they never acknowledge your bug they probably aren't paying too much attention to this anyway....the deprecated API seems very unlikely to break anytime soon.

Getting clicked item index in NSPathControl with pathItems instead of pathComponentCells
 
 
Q