Show contextual menu on ctrl-click/right-click on header of NSTableView - objective-c

I'm searching for an elegant way to detect a right-click/ctrl-click on the header of an NSTableView.
When the right click occurs, I want to display an contextual menu.
- (NSMenu *)menuForEvent:(NSEvent *)
detects only right clicks in the table - not in the header of the table.
thanks for your help.

Sometimes a picture explains a 1000 words.
You do not need to subclass your table view.
On any tableView you can select the TableView and connect the menu outlet to a menu.
Now you can wire the selector of the menu (on the right) to your code .
To figure out what row in the table was clicked use
[yourTableView clickedRow]
Done. Like a boss.

Get the NSTableHeaderView from the NSTableView and set it's menu.
[[myTableView headerView] setMenu:aMenu];

You need to subclass NSTableHeaderView. While it is possible to make a menu show up without subclassing, it is not possible to find out which table column was clicked without subclassing (making the context menu useless).
I wrote my own sublcass of the table header view, and added a delegate. In interface builder, find the NSTableHeaderView, assign it your custom subclass, and connect its new delegate outlet. Additionally, create a menu and assign it to the menu outlet.
Then implement the -validateMenu:forTableColumn: method in the delegate. Enable/disable menu items as apropriate (make sure that the menu doesn't autovalidate in IB). Store the clicked column somewhere in an instance variable, so you know which column to act on when the user selects an action.
PGETableViewTableHeaderView.h
#import <Cocoa/Cocoa.h>
#protocol PGETableViewTableHeaderViewDelegate <NSObject>
-(void)validateMenu:(NSMenu*)menu forTableColumn:(NSTableColumn*)tableColumn;
#end
#interface PGETableViewTableHeaderView : NSTableHeaderView
#property(weak) IBOutlet id<PGETableViewTableHeaderViewDelegate> delegate;
#end
PGETableViewTableHeaderView.m
#import "PGETableViewTableHeaderView.h"
#implementation PGETableViewTableHeaderView
-(NSMenu *)menuForEvent:(NSEvent *)event {
NSInteger columnForMenu = [self columnAtPoint:[self convertPoint:event.locationInWindow fromView:nil]];
NSTableColumn *tableColumn = nil;
if (columnForMenu >= 0) tableColumn = self.tableView.tableColumns[columnForMenu];
NSMenu *menu = self.menu;
[self.delegate validateMenu:menu forTableColumn:tableColumn];
return menu;
}
#end

Thanks Jakob Egger for his precise answer.
I come up with Swift version of this approach. I changed the delegate method signature a little bit, to give more flexibility in case of more then one TableView in ViewController.
protocol IMenuTableHeaderViewDelegate: class {
func menuForTableHeader(inTableView tableView: NSTableView, forTableColumn tableColumn: NSTableColumn) -> NSMenu?
}
class MenuTableHeaderView: NSTableHeaderView {
weak var menuDelegate: IMenuTableHeaderViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
guard tableView != nil else {
return nil
}
let columnForMenu = column(at: convert(event.locationInWindow, from: nil))
if columnForMenu >= 0, tableView!.tableColumns.count > columnForMenu {
if let tableColumn = tableView?.tableColumns[columnForMenu] {
return menuDelegate?.menuForTableHeader(inTableView: tableView!, forTableColumn: tableColumn)
}
}
return self.menu;
}
}
To use this custom class, find NSTableHeaderView in the interface builder and change the class to MenuTableHeaderView
Window where you have to enter custom class name
Example of this approach usage in a ViewController
class ExampleViewController: NSViewController, IMenuTableHeaderViewDelegate {
#IBOutlet weak var tableView: NSTableView!
#IBOutlet var tableHeaderMenu: NSMenu!
var lastColumnForMenu: HeaderColumnForMenu?
struct HeaderColumnForMenu {
let tableView: NSTableView
let tableColumn: NSTableColumn
}
override func viewDidLoad() {
super.viewDidLoad()
if let tableHeaderWithMenu = tableView.headerView as? MenuTableHeaderView {
tableHeaderWithMenu.menuDelegate = self
}
}
func menuForTableHeader(inTableView tableView: NSTableView, forTableColumn tableColumn: NSTableColumn) -> NSMenu? {
//Save column to wich we are going to show menu
lastColumnForMenu = HeaderColumnForMenu(tableView: tableView, tableColumn: tableColumn)
if needShowMenu {
return tableHeaderMenu
}
return nil
}
}

Related

NSTextField and NSTouchbar

I am implementing touchbar functionalities.
I want a specific touchbar to be displayed when editing an NSTextField.
I have tried to both methods :
set a touchbar using touchbar property :
field.touchBar = myTouchBar
and subclassing NSTextField to override makeTouchBar() function :
class MyTextField: NSTextField
{
override func makeTouchBar(){return myTouchBar}
}
Both methods show an empty touchbar when editing the field. Changing the isAutomaticTextCompletionEnabled and allowsCharacterPickerTouchBarItem properties does not change it - just making the corresponding buttons appear.
Doing exactly the same thing with an NSTextView - or many other type of NSView, however, works perfectly well.
Do you know if it is possible to have a custom toolbar when editing an NSTextField?
Thanks to #Willeke's answer, I have been able to find the solution. It is quite tricky, however :
First, subclass NSTextField to keep another NSTouchBar :
class MyTextField: NSTextField
{
private var innerTouchBar: Any?
var editor: NSText?
#available(OSX 10.12.2, *)
func setTouchBar(_ touchBar: NSTouchBar?)
{
innerTouchBar = touchBar
}
#available(OSX 10.12.2, *)
func getTouchBar() -> NSTouchBar?
{
innerTouchBar as? NSTouchBar
}
}
Then, subclass NSTextView to use a provided NSTouchBar :
#available(OSX 10.12.2, *)
class MyTextView: NSTextView
{
private var innerTouchBar: NSTouchBar?
convenience init(touchBar: NSTouchBar?)
{
self.init()
innerTouchBar = touchBar
}
override func makeTouchBar() -> NSTouchBar?
{
innerTouchBar
}
}
When the NSWindowController gets asked for the NSTextView of the NSTextField, then create a custom NSTextView with the innerTouchBar of the NSTextField :
extension MyWindowController
{
func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any?
{
if #available(OSX 10.12.2, *)
{
if let field = client as? MyTextField
{
if field.editor == nil
{
field.editor = SFTextView(touchBar: field.getTouchBar())
field.editor?.isFieldEditor = true
}
return field.editor
}
}
return nil;
}
}
Of course, do not forget to use MyTextField instead of NSTextField in the XIB or in your code, and to call the setTouchBar(_:) function first thing after creation.
Explanation
Every NSTextField has an underlying NSTextView, which is in fact the firstResponder, and the object whose touchBar is displayed. We cannot access the underlying NSTextView directly from NSTextField. Instead, the NSTextField asks the NWindowController which NSTextView to use. So when this happens, in windowWillReturnFieldEditor(_:,to:) of NSWindowController, we have to return a custom NSTextViewwith the correct touchBar.
I think this can apply to other things than touchBar...

NSCollectionView doesn't popup context menu?

I've bounded the menu to the NSCollectionView in interface builder. But when I CTRL+click (right click) on it the menu is not showing.
I've tried adding some method to the NSCollectionView subclass. None of them is invoked:
+ (NSMenu*)defaultMenu
- (NSMenu *)menuForEvent:(NSEvent *)theEvent
- (void)rightMouseDown:(NSEvent *)theEvent
- (void)sendEvent:(NSEvent *)theEvent
The only method which is invoked is:
- (NSView *)hitTest:(NSPoint)aPoint
Which means that the NSCollectionView receives the mouse events.
I've also tried to add the same methods to the subclass of NSCollectionViewItem, and the result is the same. Only hitTest: is called.
I had the same issue with the "new" NSCollectionView. The contextual menu is set up in the xib, and it is actually correctly triggered by an actual right-click on the mouse, and also by double-finger tab on the trackpad (if the user has set that option in the System Preferences), but not by control-click. Thus it seems like a legitimate bug or limitation with NSCollectionView, maybe dependent on how it is set up.
In any case, here is a shorter solution, this one in Swift, that assumes you have otherwise set up the contextual menu using the menu outlet for the collection view (or you have it set up as outlined in Apple's documentation).
You will need to create a subclass of NSCollectionView and choose the subclass for the collection view in the xib. Here is the code for the subclass:
import Cocoa
class MyCollectionView: NSCollectionView {
/// Fixes the behavior of collection view with control-click, that does not properly trigger the contextual menu.
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if event.type == .rightMouseDown || event.modifierFlags.contains(.control) {
rightMouseDown(with: event)
}
}
}
This works for me:
#interface MyCollectionView : NSView
-(void)mouseDown:(NSEvent *)theEvent;
#end
#implementation MyCollectionView
-(void)mouseDown:(NSEvent *)theEvent
{
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:#"Contextual Menu"];
[theMenu insertItemWithTitle:#"Beep" action:#selector(beep) keyEquivalent:#"" atIndex:0];
[theMenu insertItemWithTitle:#"Honk" action:#selector(honk) keyEquivalent:#"" atIndex:1];
[NSMenu popUpContextMenu:theMenu withEvent:theEvent forView:self];
[super mouseDown:theEvent];
}
-(void)beep{
}
-(void)honk{
}
#end
I hope this helps.
Subclass the NSCollectionView
class OSCollectionView: NSCollectionView {
override func menu(for event: NSEvent) -> NSMenu? {
print("menu() called")
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Create a clone", action: #selector(clone(_:)), keyEquivalent: ""))
return menu
}
#objc
func clone(_ sender: Any){
//editDelegate?.terminateAndReplace(self)
print("Clone item")
}
}

Using a Table View in a View Controller and wiring it up

I'm fairly new to xcode and Objective-C. Here is my problem:
I have a view controller with buttons and links to other view controllers on it.
On this view controller I have added a table view in which the cells will be used like a form
the cells will have text fields and labels
When trying to set this up and building it, it gives me an error saying I need to wire up my table view to the view controller somehow.
I know it is something to do with the data source and the table view delegate but I don't know how to wire the table view to the data source and delegate of my view controller.
Could anyone tell me how, or link me to an easy to follow guide on this?
Thanks
The easiest way would be to create a new Swift, or Objective-C Class and extend UITableViewController with it. This will create you a perfect sample code on how to write a UITableView DataSource and Delegate, which could be just copied.
After that, set your UITableViews delegate and datasource properties to self in viewdidload and implement UITableViewDataSource, UITableViewDelegate.
Edit
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var myTable: UITableView!
var myDataArray: NSArray!
override func viewDidLoad() {
super.viewDidLoad()
myDataArray = NSArray(objects: "Peter", "Paul", "Marry")
myTable.dataSource = self
myTable.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//MARK: TableView DataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myDataArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
if (cell == nil) {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
cell?.textLabel.text = myDataArray.objectAtIndex(indexPath.row) as NSString
return cell!
}
//MARK: TableView Delegate
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
I quickly wired you up some Swift Example code, where you can see how to connect a table view, with the DataSource and Delegate of your Class.

Contextual menu on only certain items in a "Source List"

I have a window with a Source List (NSOutlineView). My source list has just two levels. Level one is header and level two is data. I want to have a contextual menu on some of the data cells. Not all.
First, I try to attach a menu on the table cell view who represents the data cell -> nothing happens.
Second, I attach a menu on the Outline View in IB -> the contextual menu opens on each cells (header and data). I search for stopping the opening of the menu, but I don't find anything.
Do you have some ideas ?
Thank you
OS X 10.8.2 Lion, Xcode 4.5.2, SDK 10.8
If you subclass NSOutlineView, you can override menuForEvent: to return a menu only if the user clicked on the correct row. Here's an example:
- (NSMenu *)menuForEvent:(NSEvent *)event;
{
//The event has the mouse location in window space; convert it to our (the outline view's) space so we can find which row the user clicked on.
NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
NSInteger row = [self rowAtPoint:point];
//If the user did not click on a row, or is not exactly one level down from the top level of hierarchy, return nil—that is, no menu.
if ( row == -1 || [self levelForRow:row] != 1 )
return nil;
//Create and populate a menu.
NSMenu *menu = [[NSMenu alloc] init];
NSMenuItem *delete = [menu addItemWithTitle:NSLocalizedString( #"Delete", #"" ) action:#selector(delete:) keyEquivalent:#""];
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
//Set the Delete menu item's represented object to the clicked-on item. If the user chooses this item, we'll retrieve its represented object so we know what to delete.
[delete setRepresentedObject:[self itemAtRow:row]];
return menu;
}
This assumes we're compiling with ARC, so you don't need to autorelease the menu object being created.
This extension + subclass (both NSOutlineView and NSTableView) does the sensible thing of seeing whether a menu is attached to a cell view or row view. Just a general, reusable subclass!
Set the menu on the cell view in outlineView:viewForTableColumn:item: – menu is a NSResponder property.
(Below is in Swift)
// An extension lets us both subclass NSTableView and NSOutlineView with the same functionality
extension NSTableView {
// Find a cell view, or a row view, that has a menu. (e.g. NSResponder’s menu: NSMenu?)
func burnt_menuForEventFromCellOrRowViews(event: NSEvent) -> NSMenu? {
let point = convertPoint(event.locationInWindow, fromView: nil)
let row = rowAtPoint(point)
if row != -1 {
if let rowView = rowViewAtRow(row, makeIfNecessary: true) as? NSTableRowView {
let column = columnAtPoint(point)
if column != -1 {
if let cellView = rowView.viewAtColumn(column) as? NSTableCellView {
if let cellMenu = cellView.menuForEvent(event) {
return cellMenu
}
}
}
if let rowMenu = rowView.menuForEvent(event) {
return rowMenu
}
}
}
return nil
}
}
class OutlineView: NSOutlineView {
override func menuForEvent(event: NSEvent) -> NSMenu? {
// Because of weird NSTableView/NSOutlineView behaviour, must set receiver’s menu otherwise the target cannot be found
self.menu = burnt_menuForEventFromCellOrRowViews(event)
return super.menuForEvent(event)
}
}
class TableView: NSTableView {
override func menuForEvent(event: NSEvent) -> NSMenu? {
// Because of weird NSTableView/NSOutlineView behaviour, must set receiver’s menu otherwise the target cannot be found
self.menu = burnt_menuForEventFromCellOrRowViews(event)
return super.menuForEvent(event)
}
}
It's not clear from your question whether your outline is view based or cell based. That's important.
If you're view based, then your view instances can implement
- (NSMenu *)menuForEvent:(NSEvent *)theEvent
and return the menu appropriate to that item -- or nil f you don't want a menu at all.
If you're cell based, or if you don't want to handle this in the view class for some reason, you'll need to subclass NSOutlineView and implement - (NSMenu *)menuForEvent:(NSEvent *)theEvent there. Again, you'll figure out which cell is hit or active, and decide from that what menu you want.
- (void)rightMouseDown:(NSEvent *)event
An NSView will not pass this to the next view, This method looks to see that the current class has a menuForEvent:, if it does then it is called. If it does not then it is finished and nothing else will happen. This is why you will not see an NSTableCellView respond to a menuForEvent: because the table view swallows the rightMouseDown:.
You may subclass the tableview and handle the rightMouseDown: event and call the NSTableCellView's rightMouseDown: and handle displaying your menu that you have constructed in your storyboard and hooked up to your NSTableViewCell.
Here is my solution in a subclassed NSTableView:
- (void)rightMouseDown:(NSEvent *)event
{
for (NSTableRowView *rowView in self.subviews) {
for (NSView *tableCellView in [rowView subviews]) {
if (tableCellView) {
NSPoint eventPoint = [event locationInWindow];
// NSLog(#"Window Point: %#", NSStringFromPoint(eventPoint));
eventPoint = [self convertPoint:eventPoint toView:nil];
eventPoint = [self convertPoint:eventPoint toView:self];
// NSLog(#"Table View Point: %#", NSStringFromPoint(eventPoint));
NSRect newRect = [tableCellView convertRect:[tableCellView bounds] toView:self];
// NSLog(#"Rect: %#", NSStringFromRect(newRect));
BOOL rightMouseDownInTableCellView = [tableCellView mouse:eventPoint inRect:newRect];
// NSLog(#"Mouse in view: %hhd", mouseInView);
if (rightMouseDownInTableCellView) {
if (tableCellView) {
// Lets be safe and make sure that the object is going to respond.
if ([tableCellView respondsToSelector:#selector(rightMouseDown:)]) {
[tableCellView rightMouseDown:event];
}
}
}
}
}
}
}
This will find where the right mouse event occurred, check to see if we have the correct view and pass the rightMouseDown: to that view.
Please let me know if this solution works for you.

NSTableView Right Clicked Row Index

I'm looking for a way to get right-clicked row index from NSTableView but I can't find any delegate methods or class attributes for it. Any suggestion is appreciated.
Use the NSTableView method - (NSInteger)clickedRow to get the index of the last clicked row. The returned NSInteger will be the index of the right clicked row.
You do not need to subclass NSTableView for this solution. clickedRow is also available on NSOutlineView.
While I haven't done this, I am pretty sure you can by overriding NSView's - (NSMenu*)menuForEvent:(NSEvent*)theEvent. The example in this link does a point conversion to determine the index.
-(NSMenu*)menuForEvent:(NSEvent*)theEvent
{
NSPoint mousePoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
int row = [self rowAtPoint:mousePoint];
// Produce the menu here or perform an action like selection of the row.
}
Updated Answer
If you want to get clicked row index on menu opening, the answer is NSTableView.clickedRow. Anyway this property is available only in specific moments, and usually just -1.
When is this index to be available? That's in NSMenuDelegate.menuWillOpen method. So you conform the delegate and implement the method on your class, and access the clickedRow property. It's done.
final class FileNavigatorViewController: NSViewController, NSMenuDelegate {
let ov = NSOutlineView() // Assumed you setup this properly.
let ctxm = NSMenu()
override func viewDidLoad() {
super.viewDidLoad()
ov.menu = ctxm
ctxm.delegate = self
}
func menuWillOpen(_ menu: NSMenu) {
print(outlineView.clickedRow)
}
}
Clicked row index is available until you click an item in the menu. So this also works.
final class FileNavigatorViewController: NSViewController {
let ov = NSOutlineView() // Assumed you setup this properly.
let ctxm = NSMenu()
let item1 = NSMenuItem()
override func viewDidLoad() {
super.viewDidLoad()
ov.menu = ctxm
ov.addItem(item1)
ov.target = self
ov.action = #selector(onClickItem1(_:))
}
#objc
func onClickItem1(_: NSObject?) {
print(outlineView.clickedRow)
}
}
I tested this on macOS Sierra (10.12.5).
Old Answer
Starting from OS X 10.11, Apple finally added a method to access clickedRow easily. Just subclass NSTableView and override this method and you'll get the clickedRow as far as I experienced.
func willOpenMenu(menu: NSMenu, withEvent event: NSEvent)
This needs subclassing, but anyway, the cleanest and simplest way to access clickedRow.
Also, there's a pairing method.
func didCloseMenu(menu: NSMenu, withEvent event: NSEvent?)
Just select row on right-click by implementing menuForEvent: in NSTableView subclass:
#implementation MyTableView
- (NSMenu *)menuForEvent:(NSEvent *)theEvent
{
int row = [self rowAtPoint:[self convertPoint:theEvent.locationInWindow fromView:nil]];
if (row == -1)
return nil;
if (row != self.selectedRow)
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
return self.menu;
}
#end
if you dont need to open NSMenu but need to know "right click action with row number", i think most simple way is below. (Swift4 Code) Don't need any other connected Outer NSMenu class.
class SomeViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
...
override func viewDidLoad() {
...
tableView.action = #selector(some method()) // left single click action
tableView.doubleAction = #selector(someMethod()) // left double click action
}
// right click action
override func rightMouseDown(with theEvent: NSEvent) {
let point = tableView.convert(theEvent.locationInWindow, from: nil)
let row = tableView.row(at: point)
print("right click")
print(row)
}
I had the same question but I also needed a solution that would work with multiple selected rows (because when multiple rows are selected and you right-click on one of them, NSTableView highlights all of them). Here's the property I added for this in a subclass of NSTableView:
var rightClickRowIndexes: IndexSet {
if clickedRow >= 0 {
return selectedRowIndexes.contains(clickedRow) ? selectedRowIndexes : IndexSet(integer: clickedRow)
} else {
return IndexSet()
}
}