28

I'm trying to detect when a mouse click occurs in an NSTableView, and when it does, to determine the row and column of the cell that was clicked.

So far I've tried to use NSTableViewSelectionDidChangeNotification, but there are two problems:

  1. It only triggers when the selection changes, whereas I want every mouse click, even if it is on the currently selected row.
  2. The clickedRow and clickedColumn properties of NSTableView are both -1 when my delegate is called.

Is there a better (and correct) way of doing this?

bright
  • 4,700
  • 1
  • 34
  • 59

6 Answers6

74

There is a simple way.

Tested with Swift 3.0.2 on macOS 10.12.2 and Xcode 8.2.1

Let

tableView.action = #selector(onItemClicked)

Then

@objc private func onItemClicked() {
    print("row \(tableView.clickedRow), col \(tableView.clickedColumn) clicked")
}
longkai
  • 3,598
  • 3
  • 22
  • 24
  • 3
    Seems the best answer. – Kjuly Apr 04 '17 at 11:30
  • 4
    Just be sure to set `tableView.target` too. – sam Jun 01 '18 at 22:37
  • 2
    Brilliant! This is the best answer indeed – rmvz3 Nov 20 '18 at 20:38
  • 1
    If you declare the method like `@IBAction private func tableRowWasClicked(_ tableView: NSTableView)`, then in your XIB or storyboard, you can connect the table view's `action` outlet to the method intead of setting the `target` and `action` in code. – rob mayoff Jun 23 '20 at 20:06
  • Seems it not only a good answer, it is the way it supposed to work. Because implementing mouseDown and similar have to deal with a lot of coordinates that shift when the table is placed inside a NSScrollView. – Ol Sen May 23 '21 at 22:35
  • Excellent answer and it appears not to be limited to Swift, but also applies to Objective C – Guruniverse May 01 '22 at 23:54
27

To catch the user clicking a row (only, when the user clicks a row, not when it is selected programmatically) :

Subclass your NSTableView and declare a protocol

MyTableView.h

@protocol ExtendedTableViewDelegate <NSObject>

- (void)tableView:(NSTableView *)tableView didClickedRow:(NSInteger)row;

@end

@interface MyTableView : NSTableView

@property (nonatomic, weak) id<ExtendedTableViewDelegate> extendedDelegate;

@end

MyTableView.m

Handle the mouse down event (note, the delegate callback is not called when the user clicks outside, maybe you want to handle that too, in that case, just comment out the condition "if (clickedRow != -1)")

- (void)mouseDown:(NSEvent *)theEvent {

    NSPoint globalLocation = [theEvent locationInWindow];
    NSPoint localLocation = [self convertPoint:globalLocation fromView:nil];
    NSInteger clickedRow = [self rowAtPoint:localLocation];

    [super mouseDown:theEvent];

    if (clickedRow != -1) {
        [self.extendedDelegate tableView:self didClickedRow:clickedRow];
    }
}

Make your WC, VC conform to ExtendedTableViewDelegate.

@interface MyViewController : DocumentBaseViewController<ExtendedTableViewDelegate, NSTableViewDelegate,  NSTableViewDataSource>

set the extendedDelegate of the MyTableView to your WC, VC (MyViewController)

somewhere in MyTableView.m

self.myTableView.extendedDelegate = self

Implement the callback in delegate (MyViewController.m)

- (void)tableView:(NSTableView *)tableView didClickedRow:(NSInteger)row {
    // have fun
}
Anoop Vaidya
  • 46,283
  • 15
  • 111
  • 140
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
9

I would prefer doing as follows.

Override

-(BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row;

Provide super implementation;

RequiredRow = row;
RequiredColumn = [tableView clickedColumn];

Hope this helps.

Suhas Aithal
  • 842
  • 8
  • 20
  • 2
    this is triggered not only by the user action, as stated in the question "I'm trying to detect when a mouse click occurs in an NSTableView, and when it does, to determine the row and column of the cell that was clicked" but also every time the selection changes programaticly – Peter Lapisu Oct 29 '13 at 14:29
  • 1
    This also gets triggered when the user does actions other than click, e.g. pressing Spacebar or typing a letter key while the list has focus. – peterflynn Sep 28 '15 at 22:01
7

If someone is looking for a Swift 3/4/5 version of Peter Lapisu's answer:

Add an extension for the NSTableView (NSTableView+Clickable.swift):

import Foundation
import Cocoa

extension NSTableView {
    open override func mouseDown(with event: NSEvent) {
        let globalLocation = event.locationInWindow
        let localLocation = self.convert(globalLocation, from: nil)
        let clickedRow = self.row(at: localLocation)

        super.mouseDown(with: event)

        if (clickedRow != -1) {
            (self.delegate as? NSTableViewClickableDelegate)?.tableView(self, didClickRow: clickedRow)
        }
    }
}

protocol NSTableViewClickableDelegate: NSTableViewDelegate {
    func tableView(_ tableView: NSTableView, didClickRow row: Int)
}

Then to use it, make sure you implement the new delegate protocol:

extension MyViewController: NSTableViewClickableDelegate {
    @nonobjc func tableView(_ tableView: NSTableView, didClickRow row: Int) {
        Swift.print("Clicked row \(row)")
    }
}

The @nonobjc attribute silences the warning about it being close to didClick.

Noah Gilmore
  • 1,319
  • 2
  • 15
  • 24
Jeff Rafter
  • 159
  • 1
  • 2
4

Just in case someone was looking for it in SWIFT and / or for NSOutlineView.

Based on @Peter Lapisu instructions.

class MYOutlineViewDelegate: NSOutlineView, NSOutlineViewDelegate,NSOutlineViewDataSource{
//....
}    
extension MYOutlineViewDelegate{
    func outlineView(outlineView: NSOutlineView, didClickTableRow item: AnyObject?) {
        //Click stuff
    }

    override func mouseDown(theEvent: NSEvent) {
        let globalLocation:NSPoint  = theEvent.locationInWindow
        let localLocation:NSPoint  = self.convertPoint(globalLocation, fromView: nil)
        let clickedRow:Int = self.rowAtPoint(localLocation)

        super.mouseDown(theEvent)

        if (clickedRow != -1) {
            self.outlineView(self, didClickTableRow: self.itemAtRow(clickedRow))
        }
    }}
-2

see the tableViewSelectionIsChanging notification, here are the the comments from NSTableView.h

/* Optional - Called when the selection is about to be changed, but note, tableViewSelectionIsChanging: is only called when mouse events are changing the selection and not keyboard events. */

I concede that this might not be the surest way to correlate your mouse clicks, but it is another area to investigate, seeing that you are interested in mouse clicks.

MOK9
  • 375
  • 4
  • 9
  • I don't think this covers this requirement from the question: "I want every mouse click, even if it is on the currently selected row." – peterflynn Sep 28 '15 at 22:03
  • This will fail to fire if you click again on the currently selected row... no good – Jc Nolan Aug 14 '22 at 18:32