简体   繁体   中英

NSTableView with NSCheckBox cells - how to intercept row selection?

I have a NSTableView with cells that contain NSCheckBox . I'd like to change the behaviour of it so that any row in the table can be clicked and the checkbox in that row toggles on/off accordingly.

I figured it can be done with

    func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
}

This method provides the index of the clicked row and I could use this to toggle the checkboxes from there on programmatically. But the problem is that the NSCheckBox intercepts mouse clicks on its row. Is there any way to disable this so that the rows can be clicked fully? Setting the enabled state of the NSCheckBox to false would allow this but it also greys out the checkbox and its title.

EDIT:

To clarify what I need: The (view-based) table cell view contains a checkbox. If a user clicks the checkbox, it toggles but when the row is clicked anywhere where no checkbox is, nothing happens. I want the row to be clicked and the checkbox toggles accordingly. So essentially I want the checkbox to be non-clickable and the table row to notify the checkbox inside it when the row is clicked.

In a cell-based table view, you can implement -selectionShouldChangeInTableView: . I assume this will also work for a view-based table view.

- (BOOL)selectionShouldChangeInTableView:(NSTableView *)tableView
{
    NSInteger clickedColumn = tableView.clickedColumn;
    NSInteger attributeEnabledColumn = [tableView columnWithIdentifier:@"attributeEnabled"];

    if (clickedColumn == attributeEnabledColumn) {
        NSInteger clickedRow = tableView.clickedRow;

        if (clickedRow >= 0) {
            NSArrayController  *attributesArrayController = self.attributesArrayController;
            NSMutableDictionary *attributeRow = [attributesArrayController.arrangedObjects objectAtIndex:clickedRow];
            BOOL attributeEnabled = [attributeRow[kAttributeSelectionAttributeEnabledKey] boolValue];

            attributeRow[kAttributeSelectionAttributeEnabledKey] = @(!attributeEnabled);
        }

        return NO;
    }

    return YES;
}

Your approach is fine so far, you can click on the table row and this toggles the checkbox state.

As you said, the checkbox can be clicked on its own which doesn't trigger the table row selection. You need to subclass the NSTableCellView and assign this subclass to the cell's class property. Within that custom class file you can react on the checkbox toggle and change the underlying datasource of your table.

import Cocoa

class MyTableCellView: NSTableCellView {
    @IBOutlet weak var checkbox: NSButton!  // the checkbox

    @IBAction func checkboxToggle(sender: AnyObject) {
        // modify the datasource here
    }
}

EDIT:

Here is a code snippet where a checkbox is toggled when the user clicks anywhere on the table cell:

func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
    if let cell = tableView.viewAtColumn(0, row: row, makeIfNecessary: false) as? MyTableCellView {
        cell.checkbox.state = cell.checkbox.state == NSOnState ? NSOffState : NSOnState
    }

    return false
}

Note that it still needs a subclass for the table cell where you place an @IBOutlet to your checkbox to make it accessible in code.

Apple provide you with the opportunity to intercept a number of NSEvent types via the following NSEvent class method:

在此处输入图片说明

Any time an event whose type property matches the mask you passed in to the first argument of the above method, the block (second argument) executes. This block gives you the opportunity to do a number of things: you can carry out additional processing then let the event carry on as usual, you can modify the event, or you cancel the event altogether. Crucially anything you put in this block happens before any other event processing .

In the snippet below any time a checkbox is clicked, the incoming event is doctored to make the event behave as it the click took place outside of the checkbox, but inside the checkbox's NSTableCellView superview.


func applicationDidFinishLaunching(aNotification: NSNotification) {

    NSEvent.addLocalMonitorForEventsMatchingMask(.LeftMouseDownMask,
        handler: { (theEvent) -> NSEvent! in

        var retval: NSEvent? = theEvent
        let winPoint = theEvent.locationInWindow

        // Did the mouse-down event take place on a row in the table?
        if let row = self.tableView.rowContainingWindowPoint(winPoint) {

            // Get the view on which the mouse down event took place
            let view = self.tableView.viewAtColumn(0,
                row: row,
                makeIfNecessary: true) as! NSTableCellView

            // In my demo-app the checkbox is the NSTableCellView's last subview
            var cb = view.subviews.last! as! NSButton
            let cbBoundsInWindowCoords = cb.convertRect(cb.bounds, toView: nil)

            // Did the click occur on the checkbox part of the view?
            if CGRectContainsPoint(cbBoundsInWindowCoords, theEvent.locationInWindow) {

                // Create a modified event, where the <location> property has been 
                // altered so that it looks like the click took place in the 
                // NSTableCellView itself.
                let newLocation = view.convertPoint(view.bounds.origin, toView: nil)
                retval = theEvent.cloneEventButUseAdjustedWindowLocation(newLocation)
            }
        }

        return retval

    })
}

func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {

    if let view = self.tableView.viewAtColumn(0,
        row: row,
        makeIfNecessary: true) as? NSTableCellView {

        var checkbox = view.subviews.last! as! NSButton
        checkbox.state = (checkbox.state == NSOnState) ? NSOffState : NSOnState
    }
    return true
}

////////////////////////////////////////////////////////////////

extension NSTableView {

    // Get the row number (if any) that coincides with a specific
    // point - where the point is in window coordinates.
    func rowContainingWindowPoint(windowPoint: CGPoint) -> Int? {
        var rowNum: Int?
        var tableRectInWindowCoords = convertRect(bounds, toView: nil)
        if CGRectContainsPoint(tableRectInWindowCoords, windowPoint) {
            let tabPt = convertPoint(windowPoint, fromView: nil)
            let indexOfClickedRow = rowAtPoint(tabPt)
            if indexOfClickedRow > -1 && indexOfClickedRow < numberOfRows {
                rowNum = indexOfClickedRow
            }
        }
        return rowNum
    }
}

extension NSEvent {

    // Create an event based on another event. The created event is identical to the
    // original except for its <location> property.
    func cloneEventButUseAdjustedWindowLocation(windowLocation: CGPoint) -> NSEvent {
        return NSEvent.mouseEventWithType(type,
            location: windowLocation,
            modifierFlags: modifierFlags,
            timestamp: timestamp,
            windowNumber: windowNumber,
            context: context,
            eventNumber: eventNumber,
            clickCount: clickCount,
            pressure: pressure)!
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM