Which NSTableView receives NSMenuItem? - macos

I have a custom NSViewController with two NSTableViews side-by-side, something like a split-view setup where the selection on the left tableView changes the right tableView's list. I'm not sure how to handle NSMenuItem events in this case. For e.g. if I press the Delete button, how do I distinguish between whether it's the left tableview or the right tableview that was highlighted when the Delete button was pressed? All I get is the delete: selector called with the NSMenuItem as the sender.

First, some background:
In Cocoa terminology, the "active" view or control is known as the "first responder." For instance, when you're entering text into a text field, the text field is considered the "first responder" because it's the object that's first to respond to keyboard input. An NSTableView can also receive first responder status (you can control the selected row by using the arrow keys).
You can ask the window for it's first responder like so:
// it's not necessarily a sure thing that the first responder is a TableView.
id myFirstResponder = [_parentWindow firstResponder];
if (myFirstResponder == _leftTableView) {
// left tableview is selected
} else if (myFirstResponder == _rightTableView) {
// right tableview is selected
}

Related

How to create an NSMenu containing an NSMenuItem which only appears while holding a keyboard modifier key?

I'd like to create an NSMenu containing an NSMenuItem which is hidden by default, and only appears while the user is holding a keyboard modifier key.
Basically, I'm looking for the same behaviour as the 'Library' option in the Finder's 'Go' Menu:
Without holding Option (⌥):
While holding Option (⌥):
I already tried installing a key listener using [NSEvent addGlobalMonitorForEventsMatchingMask: handler:] to hide and unhide the NSMenuItem programmatically by setting it's hidden property. This kind of worked, but the problem is that the hiding/unhiding wouldn't work while the NSMenu was open. Apparently an NSMenu completely takes over the event processing loop while it's open, preventing the key listener from working.
I could probably use a CGEventTap to still receive events while the NSMenu is open, but that seems like complete overkill.
Another thing I discovered which does a similar thing to what I want is the 'alternate' mechanism of NSMenu. But I could only get it to switch out NSMenuItems, not hide/unhide them.
Any help would be greatly appreciated. Thanks!
Let's say your option-only menu item's action is (in Swift) performOptionOnlyMenuItem(_:) and its target is your AppDelegate.
The first thing you need to do is make sure AppDelegate conforms to the NSMenuItemValidation protocol.
The second thing you need to do is implement the validateMenuItem(_:) method, and have it check whether the menu item sends the performOptionOnlyMenuItem(_:) action. If so, set the item's isHidden property based on whether the option key is currently pressed.
If you don't need to validate any other menu items, the code can look like this:
extension AppDelegate: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
switch menuItem.action {
case #selector(performOptionOnlyMenuItem(_:)):
let flags = NSApp.currentEvent?.modifierFlags ?? []
menuItem.isHidden = !flags.contains(.option)
return true
default:
return true
}
}
}
If the action is sent to some other target, you need to implement the validation (including the protocol conformance) on that target. Each menu item is validated only by the item's target.
I found a solution that behaves perfectly!
On the NSMenuItem you want hidable, set the alternate property to YES, and set the keyEquivalentModifierMask property to the keyboard modifiers which you want to unhide the item.
In your NSMenu, right before the NSMenuItem which you want to be hideable, insert another NSMenuItem that has height 0.
In Objc, you can create an NSMenuItem with height 0 like this:
NSMenuItem *i = [[NSMenuItem alloc] init];
i.view = [[NSView alloc] initWithFrame:NSZeroRect];
The hideable NSMenuItem will now be 'alternate' to the zero-height NSMenuItem preceding it. The zero-height item will display by default, but while you hold the keyboard modifier(s) you specified, the zero-height item will be swapped out with the hideable item. Because the zero-height item is invisible, this has the effect of unhiding the hideable item.

Editable NSTextField in NSTableView

I have a view based NSTableView with some labels in my customized and subclassed view. One of the label should be editable, so therefore I set this NSTextField to editable.
But now I have two problems, I can't solve:
1) If I move the mouse over the editable NSTextField, the cursor don't change to the IBeamCursor (the edit cursor).
2) I need to double click at the label, to be able to edit. I want to have a single click. I found some solutions for this problem here at stackoverflow, the best one is to override the acceptsFirstResponder to return always true, but then, clicking at the NSTextField selects the whole text instead of placing the cursor at the clicked position.
Sorry... this is a duplicate. I found this:
NSTableView - select row and respond to mouse events immediately
You have to subclass NSTableView. My swift code:
class TableViewEditing: NSTableView {
...
override func validateProposedFirstResponder(responder: NSResponder, forEvent event: NSEvent?) -> Bool {
return true
}
}
EDIT:
Just one disadvantage: Sometimes entering the edit mode, it seems that the text is just shortly selected and deselected. But you can see, that this is a cocoa problem, it's the same for example in Apple reminders app.

Maintaining focus or first responder state of NSOutlineView

I have a subclass of an NSOutline. When it loses focus, the disclosure triangle of a selected row changes from its inverted template form (white) back to black. I don't want this to happen: I want the appearance of a selected row to stay the same, as in XCode or Mail.
I've tried intercepting firstResponder-based messages in the NSOutlineView subclass, as well in my custom rowViews, but to no avail. Any ideas?
In my subclass of NSTableRowView, doing the following works:
- (BOOL)isEmphasized {
return YES;
}

NSTableView, NSArrayController and reload only after key press?

I have the following situation:
There is one custom view inside of the first window that contains a NSTableView.
There is a second window which acts as a form for the current object behind the selection of the table view inside the first window.
Some more details:
I’ve implemented the setDoubleAction: behavior in the NSTableView that basically opens the second window
The table view is bound to the arrangedObjects of an (subclassed) NSArrayController
The specific interface elements in the second window (that opens on double click) are bound to the selection of the NSArrayController
I’ve subclassed the NSArrayController and modified the following functions:
At first I changed addObject: (or add:, this doesn’t really matter):
- (void)addObject:(id)object
{
[super addObject:object];
[self saveTemplatesToDisk];
}
Then I changed remove:
- (void)remove:(id)sender
{
[super remove:sender];
[self saveTemplatesToDisk];
}
The action that opens the preference sheet is just a one liner: [NSApp beginSheet:preferenceWindow modalForWindow:[_preferenceView window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
The code that get’s executed after the user presses the return key / OK button isn’t complicated either.
It just saves the current content of the array controller to disk and closes the second window:
- (IBAction)endPreferenceSheet:(id)sender
{
[templateArrayController saveTemplatesToDisk];
[NSApp endSheet:preferenceWindow];
[preferenceWindow orderOut:nil];
}
Finally here’s my problem / question
When I press the return key in the second window, the window closes, the data gets saved and the NSTableView gets properly reloaded without any further interaction. But when I press on the OK button with the mouse, nothing seems to happen. Here’s the interesting part: when I now select another row in the table view in the first window after the second window disappeared, the previously selected row (read: the updated object) gets properly reloaded and displays the content I’ve edited in the second window that has interface elements bound to the selection.
Basically my implementation works, but not when the user uses the mouse to close the window.
The only difference I can spot is the currentEvent, but I can’t imagine how this could change the behavior of this simple application.
When I press the OK button with the mouse: NSEvent: type=LMouseUp loc=(563.055,30.1484) time=58450.2 flags=0 win=0x0 winNum=5371 ctxt=0x0 evNum=8093 click=1 buttonNumber=0 pressure=0 subtype=NSTabletPointEventSubtype deviceID=0 x=19469 y=15838 z=0 buttons=0x0 pressure=0.000000 tilt={0.453108, -0.140629} rotation=0.000000 tangentialPressure=0.000000 vendor1-3=(0, 0, 0)
When I press return: NSEvent: type=KeyDown loc=(0,300) time=58474.8 flags=0 win=0x0 winNum=5371 ctxt=0x0 chars="
" unmodchars="
" repeat=0 keyCode=36
Any ideas how I can solve my problem?
Remember the responder chain: The keyboard event starts at the first responder, which will be the field editor, then (if that doesn't handle it) goes to the next responder, which will be the table view. The mouse event goes directly to the view that the user clicked on, which is the button.
So, the difference is that the table view handles the return event, but it never sees the mouse event. When the user clicks, you simply get an action message from the button—the table view remains in in editing mode.
The solution is to have the action method tell the controller to commit editing before proceeding with the actual action.

NSTableView navigate with arrow keys

How can I navigate through my table view with the arrow keys. Much like setAction: or setDoubleAction, but instead of reacting to clicks, react with the arrow keys moving up or down through the table.
In your table view delegate, implement tableView:shouldSelectRow:. Do whatever you want, then return YES. It'll get triggered as you select items in the table view.
I'm not sure what you mean because when I select something in a table I can move up and down in the table using the arrow keys. But if you want to customize the behavior more I have a solution. In one of my apps I wanted to detect when the return or enter key was pressed and then perform some action accordingly. I created a new class and made it a subclass of NSWindow. In interface builder I set the main window to be this class. Then I override the keyDown: method of NSWindow in that subclass. So whenever my main window is frontmost (first responder) then key presses are detected and filtered through the method. I'm sure you could do something similar for arrow presses. You might want to make your class a subclass of NSTableView instead of NSWindow depending on how you want to catch the key presses. I wanted it to work for the entire application but you may want it to work only when the table view is first responder.
- (void)keyDown:(NSEvent *)theEvent {
if ([theEvent type] == NSKeyDown) {
NSString* characters = [theEvent characters];
if (([characters length] > 0) && (([characters characterAtIndex:0] == NSCarriageReturnCharacter) || ([characters characterAtIndex:0] == NSEnterCharacter))) {
// do something here when return or enter is pressed
}
}
}
aha! Did you accidentally BREAK NSTableView by doing this?
#implementation NSTableView ( DeleteKeyCategory )
-( void ) keyDown: ( NSEvent * ) event
{
// ... do something ...
[super keyDown:event];
}
#end
For me, this had the nefarious side effect of REPLACING NSTableView's keyDown: routine, and it broke the cursor keys. (kind of like swizzling)
Lessons I've learned:
- avoid the keyDown: routine altogether.
- subclassing Apple NSControls will save work in the long run.
This is the type of mistake that makes using NSTableView very frustrating.
Maybe Apple could detect this kind of thing in the static analyzer?
Arrows are used for selection, not for performing any Action. The action that will be applied to the selected item will usually be set by the "action" or "doubleAction" property of the TableView.
Clicking on a table-row does two different things.
TRIES to select the table row (sometimes the table-row can REFUSE to be selected, that's why there is a "shouldSelect" delegate method).
If new selection took place, then the action is performed (with the tableView as the sender). There you can ask the table about the current selection and do whatever you need with it.
Please consider the situation when there are SEVERAL selected rows, or selected columns, or many other complicated situations.
In your case --- what I would recommend, is that you implement the
selectionDidChange:(NSNotigivation)notification;
NSTableView delegate call. This is called AFTER selection has changed, and you know by then the new current selection, and do whatever you want with the selected items.

Resources