mouseDown: in a custom NSTextField inside an NSTableView - cocoa

I have a view-based NSTableView. Each view in the table has a custom text field.
I'd like to fire an action when the user clicks on the text field (label) inside the table's view (imagine having a hyperlink with a custom action in each table cell).
I've created a basic NSTextField subclass to catch mouse events. However, they only fire on the second click, not the first click.
I tried using an NSButton and that fires right away.
Here's the code for the custom label:
#implementation HyperlinkTextField
- (void)mouseDown:(NSEvent *)theEvent {
NSLog(#"link mouse down");
}
- (void)mouseUp:(NSEvent *)theEvent {
NSLog(#"link mouse up");
}
- (BOOL)acceptsFirstResponder {
return YES;
}
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent {
return YES;
}
#end

Had the same problem. The accepted answer here didn't work for me. After much struggle, it magically worked when I selected "None" as against the default "Regular" with the other option being "Source List" for the "Highlight" option of the table view in IB!
Edit: The accepted answer turns out to be misleading as the method is to be overloaded for the table view and not for the text field as the answer suggests. It is given more clearly at https://stackoverflow.com/a/13579469/804616 but in any case, being more specific feels a bit hacky.

It turned out that NSTableView and NSOultineView handle the first responder status for NSTextField instances differently than for an NSButton.
The key to get the label to respond to the first click like a button is to overwrite [NSResponder validateProposedFirstResponder:forEvent:] to return YES in case of my custom text field class.
Documentation:
http://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/Reference/Reference.html#//apple_ref/occ/instm/NSResponder/validateProposedFirstResponder:forEvent:

The behavior that you're seeing is because the table view is the first responder, which it should be or the row won't change when you click on the label -- this is the behavior that a user expects when clicking on a table row. Instead of subclassing the label, I think it would be better to subclass the table view and override mouseDown: there. After calling the super's implementation of mouseDown:, you can do a hit test to check that the user clicked over the label.
#implementation CustomTable
- (void)mouseDown:(NSEvent *)theEvent
{
[super mouseDown:theEvent];
NSPoint point = [self convertPoint:theEvent.locationInWindow fromView:nil];
NSView *theView = [self hitTest:point];
if ([theView isKindOfClass:[NSTextField class]])
{
NSLog(#"%#",[(NSTextField *)theView stringValue]);
}
}
#end

In the exact same situation, embedding an NSButton with transparent set to true/YES worked for me.
class LinkButton: NSTextField {
var clickableButton:NSButton?
override func viewDidMoveToSuperview() {
let button = NSButton()
self.addSubview(button)
//setting constraints to cover the whole textfield area (I'm making use of SnapKit here, you should add the constraints your way or use frames
button.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(NSEdgeInsetsZero)
}
button.target = self
button.action = Selector("pressed:")
button.transparent = true
}
func pressed(sender:AnyObject) {
print("pressed")
}

You use window.makeFirstResponser(myTextfield) to begin editing the text field. You send this message from the override mouseDown(withEvent TheEvent:NSEvent) method

Related

NSTableView double click action not being called

I have an NSViewController which is set as the datasource and delegate of a single column tableview which uses a NSTableCellView subclass. I want to respond to a double-click on a row so I've set the table column to be not editable and have the following code:
myViewController.h
- (IBAction)didDoubleClickFolderRow:(id)sender;
myViewController.m
- (void)awakeFromNib
{
[self.table setDoubleAction:#selector(didDoubleClickFolderRow:)];
[self.table setTarget:self];
}
- (IBAction)didDoubleClickFolderRow:(id)sender
{
NSLog(#"yep a row was double-clicked!");
}
Yet the method never gets called. What am I missing?
Rather obscurely this turns out to have been caused by me overriding resignFirstResponder on another view, to get around another problem.
Not sure why a double click on a tableview fires a resignFirstResponder, but it seems to.

KeyDown not working

I'm trying to perform a certain action by pressing, for example the spacebar (anywhere). In my code, I've got the acceptsFirstResponder method and the keyDown method but I'm not getting an NSLog-message
Here the code:
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)keyDown:(NSEvent *)theEvent {
NSLog(#"test");
}
You need to put your -keyDown: method on an NSView subclass, and that NSView subclass has to be put in a window, and that window has to be on-screen, and you have to click on your view before you hit a key. Then the key will go to your view.
Check the diagram “The Path of Key Events” on this page.
In addition to implementing keyDown in your NSView, you also need to implement acceptsFirstResponder and have it return YES:
- (BOOL)acceptsFirstResponder
{
return YES;
}

identify sender of controlTextDidEndEditing

I have a view with three NSTextFields connected to a View Controller that acts as delegate for all three of them.
I have successfully implemented the controlTextDidEndEditing to intercept the text entered by the user and change a property in my model. The method though is unique in the delegate and all the three textfields trigger it. The question is, how can I identify which one of them fired the notification? I can get the NSTextView from the key "NSFieldEditor" of the notification but that doesn't really tell me which one it is. At the beginning I thought of using the NSTextField placeholder but the the method returns me a NSTextView which doesn't seem to have a placeholder property.
Any idea?
You should be able to obtain the reference to the NSTextField via [notification object]. If all three of your NSTextFields are available as delegates, it is easy to check which one triggered the event.
Like this:
- (void)controlTextDidEndEditing:(NSNotification *)aNotification {
NSTextField* textField = (NSTextField *)[aNotification object];
if (textField == textField1) {
// textField1 triggered the event
} else if (textField == textField2) {
...
} else if (textField == textField3) {
...
}
}

Show NSPopover from NSToolbarItem Button

I want to show an NSPopover from an NSToolbarItem button in my toolbar.
(i.e. positioned below the button).
Ideally, I want to pass the NSView of the button to the popover to position it.
My question is, how do I get the NSView of the NSToolbarItem?
[toolbarbutton view] always returns nil.
The answer appears to be in the video for the 2011 WWDC Session 113, "Full Screen and Aqua Changes." Basically, put an NSButton inside the NSToolbaritem and use the view of that.
A blog post is here: http://www.yellowfield.co.uk/blog/?p=33, and a sample project is on github at http://github.com/tevendale/ToolbarPopover
All in the sprit of http://xkcd.com/979!
You can send the action directly from the NSButton enclosed in the NSToolbarItem (which is what you should generally do anyways, consider segmented controls, where each segment has its own target/action), and that will do the trick.
Instead of getting the view from the IBAction sender, connect an IBOutlet directly to the toolbar item and use that to get the relative view:
In your header file:
#property (weak) IBOutlet NSToolbarItem *theToolbarItem;
#property (weak) IBOutlet NSPopover *thePopover;
In your implementation file, to show the popover:
[self.thePopover showRelativeToRect:[[self.theToolbarItem view] bounds] ofView:[self.theToolbarItem view] preferredEdge:NSMinYEdge];
This will also work for showing popups from menu item selections inside a toolbar item.
While I did achieve that the Popover was shown using the approach mentioned by Stuart Tevendale, I did run into problems when I tried to validate (enable / disable) the NSToolbarItems using the NSToolbarDelegate:
-(BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem {
BOOL enable = YES;
NSString *identifier = [toolbarItem itemIdentifier];
// This does never get called because I am using a button inside a custom `NSToolbarItem`
if ([identifier isEqualToString:#"Popover"]) {
return [self someValidationMechanism];
}
// For this the validation works when I am using a standard `NSToolbarItem`
else if ([identifier isEqualToString:#"StandardToolbarItem"]){
return [self someOtherValidationMechanism];
}
return enable;
}
So I would advise not to display a Popover from NSToolbarItem. An alternative might be to show a Page Sheet: How to show a NSPanel as a sheet

Custom NSView in NSMenuItem not receiving mouse events

I have an NSMenu popping out of an NSStatusItem using popUpStatusItemMenu. These NSMenuItems show a bunch of different links, and each one is connected with setAction: to the openLink: method of a target. This arrangement has been working fine for a long time. The user chooses a link from the menu and the openLink: method then deals with it.
Unfortunately, I recently decided to experiment with using NSMenuItem's setView: method to provide a nicer/slicker interface. Basically, I just stopped setting the title, created the NSMenuItem, and then used setView: to display a custom view. This works perfectly, the menu items look great and my custom view is displayed.
However, when the user chooses a menu item and releases the mouse, the action no longer works (i.e., openLink: isn't called). If I just simply comment out the setView: call, then the actions work again (of course, the menu items are blank, but the action is executed properly). My first question, then, is why setting a view breaks the NSMenuItem's action.
No problem, I thought, I'll fix it by detecting the mouseUp event in my custom view and calling my action method from there. I added this method to my custom view:
- (void)mouseUp:(NSEvent *)theEvent {
NSLog(#"in mouseUp");
}
No dice! This method is never called.
I can set tracking rects and receive mouseEntered: events, though. I put a few tests in my mouseEntered routine, as follows:
if ([[self window] ignoresMouseEvents]) { NSLog(#"ignoring mouse events"); }
else { NSLog(#"not ignoring mouse events"); }
if ([[self window] canBecomeKeyWindow]) { dNSLog((#"canBecomeKeyWindow")); }
else { NSLog(#"not canBecomeKeyWindow"); }
if ([[self window] isKeyWindow]) { dNSLog((#"isKeyWindow")); }
else { NSLog(#"not isKeyWindow"); }
And got the following responses:
not ignoring mouse events
canBecomeKeyWindow
not isKeyWindow
Is this the problem? "not isKeyWindow"? Presumably this isn't good because Apple's docs say "If the user clicks a view that isn’t in the key window, by default the window is brought forward and made key, but the mouse event is not dispatched." But there must be a way do detect these events. HOW?
Adding:
[[self window] makeKeyWindow];
has no effect, despite the fact that canBecomeKeyWindow is YES.
Add this method to your custom NSView and it will work fine with mouse events
- (void)mouseUp:(NSEvent*) event {
NSMenuItem* mitem = [self enclosingMenuItem];
NSMenu* m = [mitem menu];
[m cancelTracking];
[m performActionForItemAtIndex: [m indexOfItem: mitem]];
}
But i'm having problems with keyhandling, if you solved this problem maybe you can go to my question and help me a little bit.
Add this to your custom view and you should be fine:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
I added this method to my custom view, and now everything works beautifully:
- (void)viewDidMoveToWindow {
[[self window] becomeKeyWindow];
}
Hope this helps!
I've updated this version for SwiftUI Swift 5.3:
final class HostingView<Content: View>: NSHostingView<Content> {
override func viewDidMoveToWindow() {
window?.becomeKey()
}
}
And then use like so:
let item = NSMenuItem()
let contentView = ContentView()
item.view = HostingView(rootView: contentView)
let menu = NSMenu()
menu.items = [item]
So far, the only way to achieve the goal, is to register a tracking area manually in updateTrackingAreas - that is thankfully called, like this:
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect: bounds, options: [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: nil)
addTrackingArea(trackingArea)
}
Recently I needed to show a Custom view for a NSStatusItem, show a regular NSMenu when clicking on it and supporting drag and drop operations on the Status icon.
I solved my problem using, mainly, three different sources that can be found in this question.
Hope it helps other people.
See the sample code from Apple named CustomMenus
In there you'll find a good example in the ImagePickerMenuItemView class.
It's not simple or trivial to make a view in a menu act like a normal NSMenuItem.
There are some real decisions and coding to do.

Resources