objective-cmacosswiftnstableviewnscell

Create and Respond to a Hyperlink within a NSTableView Text Cell


I have a program that has a NSTableView populated with files to be uploaded. Once the file is sent, the Text Cell with the file's name gets a hyperlink placed into it (the array data is given an NSMutableString with an NSLinkAttributeName attribute). How do I allow users to click this link to open the webpage in their default browser?


Solution

  • After much searching and trying multiple methods, this is what I came up with as a solution.

    Creating a custom class that extends NSTableViewCell:

    class TableViewCellCursor: NSTableCellView {
    
    internal var active = false
    
    //MARK: - View Life Cycle
    
    override func awakeFromNib() {
        superview?.awakeFromNib()
        self.createTrackingArea()
    }
    
    //MARK: - IBActions
    
    override func mouseEntered(theEvent: NSEvent) {
        if (NSCursor.currentCursor() == NSCursor.arrowCursor() && active) {
            NSCursor.pointingHandCursor().set()
        }
    }
    
    override func mouseExited(theEvent: NSEvent) {
        if (NSCursor.currentCursor() == NSCursor.pointingHandCursor() && active) {
            NSCursor.arrowCursor().set()
        }
    }
    
    //Informs the receiver that the mouse cursor has moved into a cursor rectangle.
    override func cursorUpdate(event: NSEvent) {
        if (active) {
            NSCursor.pointingHandCursor().set()
        }
    }
    
    //MARK: - Util
    
    func createTrackingArea() {
        var focusTrackingAreaOptions:NSTrackingAreaOptions = NSTrackingAreaOptions.ActiveInActiveApp
        focusTrackingAreaOptions |= NSTrackingAreaOptions.MouseEnteredAndExited
        focusTrackingAreaOptions |= NSTrackingAreaOptions.AssumeInside
        focusTrackingAreaOptions |= NSTrackingAreaOptions.InVisibleRect
    
        var focusTrackingArea:NSTrackingArea = NSTrackingArea(rect: NSZeroRect,
                                                            options: focusTrackingAreaOptions,
                                                            owner: self, userInfo: nil)
        self.addTrackingArea(focusTrackingArea)
    }
    }
    

    Checking first responder status when the NSTableView selection changes. This is necessary because the table's selection can be changed, even when it is not the firstResponder:

    func tableViewSelectionDidChange(aNotification: NSNotification) {
        if (self.firstResponder == filesToTransferTable) {
            changeSelectedRowTextColorTo(NSColor.whiteColor(), unselectedColor: NSColor.blueColor())
        } else {
            changeSelectedRowTextColorTo(NSColor.blackColor(), unselectedColor: NSColor.blueColor())
        }
    }
    
    func changeSelectedRowTextColorTo(selectedColor: NSColor, unselectedColor: NSColor) {
        let selectedRows = filesToTransferTable.selectedRowIndexes
        for (index, tableEntry) in enumerate (tableData) {
            if tableData[index]["FileName"] is NSMutableAttributedString {
                var name = tableData[index]["FileName"] as! NSMutableAttributedString
                var range = NSMakeRange(0, NSString(string:name.string).length)
                name.beginEditing()
                name.removeAttribute(NSForegroundColorAttributeName, range: range)
    
                if (selectedRows.containsIndex(index)) {
                    name.addAttribute(NSForegroundColorAttributeName, value:selectedColor, range:range)
                } else {
                    name.addAttribute(NSForegroundColorAttributeName, value:unselectedColor, range:range)
                }
    
                name.endEditing()
                tableData[index]["FileName"] = name
            }
            filesToTransferTable.reloadDataForRowIndexes(NSIndexSet(index: index), columnIndexes: NSIndexSet(index:0))
        }
    }
    

    Adding KVO for checking when FirstResponder changes:

    //This is somewhere in your code where you initialize things
    //KVO for first responder behavior regarding tableView and updating attributedStrings' colors
    self.addObserver(self, forKeyPath: "firstResponder", options: NSKeyValueObservingOptions.Old | NSKeyValueObservingOptions.New, context: nil)
    
    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
                if (change[NSKeyValueChangeNewKey] is NSTableView) {
                    changeSelectedRowTextColorTo(NSColor.whiteColor(), unselectedColor: NSColor.blueColor())
                } else if (change[NSKeyValueChangeOldKey] is NSTableView) {
                    changeSelectedRowTextColorTo(NSColor.blackColor(), unselectedColor: NSColor.blueColor())
                }
            }
    

    Finally, checking if the main window (the app itself) is in focus (if this is not done, then the colors won't change appropriately when the window loses focus):

    //Put these in the same place as the KVO code
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "windowDidBecomeKey:",
                        name: NSWindowDidBecomeKeyNotification , object: self)
    
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "windowDidResignKey:",
                        name: NSWindowDidResignKeyNotification , object: self)
    
    
        func windowDidBecomeKey(notification: NSNotification) {
            if (self.firstResponder == filesToTransferTable) {
                changeSelectedRowTextColorTo(NSColor.whiteColor(), unselectedColor: NSColor.blueColor())
            } else {
                changeSelectedRowTextColorTo(NSColor.blackColor(), unselectedColor: NSColor.blueColor())
            }
        }
    
        func windowDidResignKey(notification: NSNotification) {
            if (self.firstResponder == filesToTransferTable) {
                changeSelectedRowTextColorTo(NSColor.blackColor(), unselectedColor: NSColor.blueColor())
            }
        }