Are NSPathControl and NSPathControlItem full of bugs or is it me? (yosemite)

2k Views Asked by At

Apple changed NSPathControl to work with NSPathControlItem's in Yosemite.

But from where I am sitting, these new classes don't work at all. I am trying to display a custom path in my data structures, but I have similar problems with a regular file path. Is it me or is it Apple?

Here is my code:

The first snippet works as in, it will show a path. But that is about all that works.

//MARK: notifications
func selectionDidChange(notification : NSNotification)
{
    if let item = notification.object as? Group
    {
        //get "path" components
        var components : [String] = [item.title ?? "a"]
        var ancestor : Group? = item.parent
        while (ancestor != nil)
        {
            components.append(ancestor?.title ?? "b")
            ancestor = ancestor?.parent
        }
        components.append("")

       //convert to url
        let path = ("MyScheme:/" + "/".join(components.reverse()))
        pathControl?.URL = NSURL(string: path.stringByAddingPe
     }
 }

Clicking any part of the path to try to get any property out of the NSPathControlItem does not work at all. Everything returns nil.

@IBAction func select(sender : AnyObject)
{
    println(sender.clickedPathItem??.title)
    println(sender.clickedPathItem??.URL)
}

If I try to build a path with NSPathControlItem, I can not set any properties (title, url).

    pathComponent.URL = NSURL(string: path.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!)
    //let url : NSURL? = NSURL(string: "/")
    //let path = NSURL(fileURLWithPath: "/") 
    //pathComponent.URL = path as NSURL
    pathComponent.attributedTitle = NSAttributedString(string: "/atttributes")
    self.pathControl?.pathItems = [pathComponent]
    println(pathComponent.description)

Also NSPathControlItem is not supposed to be subclassed.

What is going on here?

edit

There is a problem with NSPathControlItem as far as I can tell. A helper function to create a NSPathControlItem.

    func pathItem(title: String, imageName: String) -> NSPathControlItem
{
    let item = NSPathControlItem()
    item.title = title
    item.image = NSImage(named: imageName)
    return item
}

A test function to create NSPathControlItem's and print out their title.

    var pathItems : [NSPathControlItem] = []
    for title in ["a","b","c"]
    {
        pathItems.append(self.pathItem(title, imageName: NSImageNameFolder))
    }

    for item in pathItems
    {
        println(item.title)
    }

The expected output is three lines with a, b and c. I get nil, b, nil.

If you set the pathItems on a NSPathControl directly, it will work.

self.pathControl?.pathItems = [
self.pathItem("a", imageName: NSImageNameFolder),
self.pathItem("b", imageName: NSImageNameFolder),
self.pathItem("c", imageName: NSImageNameFolder)]

However, if you set the pathItems indirectly, all goes to hell.

self.pathControl?.pathItems = pathItems //array of NSPathControl (see above)

Edit 2

I had another look at this. I configure a NSPathControlItem in the pathItem function. Here I set the title. Makes no difference if I set the attributedTitle. Inspecting the item with lldb shows the correct (attributed)title value.

But when I assign the array of NSPathControlItem's to the NSPathControl, the title has a value of "" and the attributedTitle is uninitialized.

2

There are 2 best solutions below

0
On

NSPathControlItem is seriously broken. As of 10.11.3, it contains the following method, as decompiled by Hopper:

int -[NSPathControlItem release](int arg0) {
    [arg0->_secretCell autorelease];
    objc_assign_ivar(0x0, arg0, *_OBJC_IVAR_$_NSPathControlItem._secretCell);
    rax = [[arg0 super] release];
    return rax;
}

This is evidently supposed to be a standard dealloc override, which releases the ivar, zeroes it out, and calls super. But instead, for some unknown reason possibly related to illegal drugs, it's a release override. This means that the moment one of these objects is released, even in the course of some harmless manipulation, it self-destructs and becomes unusable.

I created some code to use the runtime to eliminate the bad release override and substitute in a proper dealloc override. This is barely tested and use at your own risk:

extension NSPathControl {
    class func fix() {
        let itemClass = NSPathControlItem.self
        let superclass: AnyClass = class_getSuperclass(itemClass)

        let releaseSelector = Selector("release")
        let releaseMethod = class_getInstanceMethod(itemClass, releaseSelector)
        let superReleaseIMP = class_getMethodImplementation(superclass, releaseSelector)

        if method_getImplementation(releaseMethod) != superReleaseIMP {
            method_setImplementation(releaseMethod, superReleaseIMP)

            let ivars = class_copyIvarList(itemClass, nil)
            var offsets: [Int] = []
            var cursor = ivars
            while cursor.memory != nil {
                let ivar = cursor.memory
                if String.fromCString(ivar_getTypeEncoding(ivar))?.hasPrefix("@") == true {
                    offsets.append(ivar_getOffset(ivar))
                }
                cursor++
            }
            free(ivars)

            let superDeallocIMP = class_getMethodImplementation(superclass, "dealloc")

            let dealloc: @convention(block) UnsafeMutablePointer<Int8> -> Void = { obj in
                for offset in offsets {
                    let ivarPtr = UnsafeMutablePointer<COpaquePointer>(obj + offset)
                    let unmanaged = Unmanaged<AnyObject>.fromOpaque(ivarPtr.memory)
                    unmanaged.release()
                    ivarPtr.memory = nil
                }

                typealias DeallocF = @convention(c) (UnsafeMutablePointer<Int8>, Selector) -> Void
                let superDeallocF = unsafeBitCast(superDeallocIMP, DeallocF.self)
                superDeallocF(obj, "dealloc")
            }
            let deallocIMP = imp_implementationWithBlock(unsafeBitCast(dealloc, AnyObject.self))

            class_addMethod(itemClass, "dealloc", deallocIMP, "v@:")
        }
    }
}
0
On

I think it is not just full of bugs, it is also not documented. The documentation for NSPathControlItem is missing in the AppKit Framework Reference .. so odd.

I'm getting the same results as you. This alternative that uses sender.clickedPathComponentCell() works ok for me though:

class ViewController: NSViewController {
    @IBOutlet weak var pathControl: NSPathControl!

    override func viewDidLoad() {
        super.viewDidLoad()
        pathControl.URL = NSURL(fileURLWithPath: "/System/Library/Fonts")
    }

    @IBAction func pathItemSelected(sender: NSPathControl) {
        if let cell = sender.clickedPathComponentCell() {
            println(cell.URL)
        }
    }
}

This correctly prints the partial paths that I clicked on.