KeyDown not working - macos

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;
}

Related

NSTableView - How to remove the Spacebar event listener?

in my Mac application, I have a NSTableView. The default behavior is that clicking on the spacebar on the keyboard it selects a row and event tableView shouldSelectRow.
In my use case however I have a button that starts and stops audio playback. I would like to remove the spacebar handling from the NSTableView and move it to the playback button.
I tried to introduce a menu item for playback and assigned the spacebar key to it. This does not work however. Every time I click the spacebar the NSTableView fires and not the menu entry.
My question is: How can I remove the spacebar listener from the NSTableView ?
thanks in advance.
It can be done using the NSTableViewDelegate protocol, avoids having to subclass NSTableView:
- (BOOL)tableView:(NSTableView *)tableView shouldTypeSelectForEvent:(NSEvent *)event withCurrentSearchString:(NSString *)searchString {
return ([event.charactersIgnoringModifiers characterAtIndex:0]!=0x20);
}
I found a solution to the problem outlined.
I overwrite my NSTableView and implemented keyDown event where I was able to intercept the spacebar key event.
#implementation AudioFileTableView
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
-(void)keyDown:(NSEvent *)theEvent{
unichar key = [[theEvent charactersIgnoringModifiers] characterAtIndex:0];
if(key == 0x0020)
{
// handle the spacebar event
return;
}
[super keyDown:theEvent];
}
#end

Allow views in childWindow to become key without losing focus on parentWindow

I added a childWindow of a custom subclass of NSWindow to a parentWindow (also a custom subclass of NSWindow). The childWindow has the NSBorderlessWindowMask and canBecomeKeyWindow: is overridden to return YES and canBecomeMainWindow: to return NO.
The childWindow is set to resize with the parentWindow. So I want to create the illusion that the views of the childWindow are part of the parentWindow. The main idea is to arrange the document windows created by the document-based application within a main window to provide a tabbed interface (just like in a browser) to switch between the documents.
My problem is that whenever I click in one of the views of the childWindow, the parentWindow (the main window) looses focus and the traffic light buttons are getting greyed out. This is obviously contrary to what I want to achieve.
I found this answer:
Make NSView in NSPanel first responder without key window status
But even if I override isKeyWindow: (of the main window) to always return YES, the title bar gets greyed out nonetheless when I click into the childWindow.
I also tried to follow this advice:
http://www.cocoabuilder.com/archive/cocoa/143945-non-focused-child-window.html
But I'm not sure what "include the child window in its responder chain just ahead of its nextResponder" means. With canBecomeKeyWindow: to return NO (for the childWindow), the views within the child never can become key and are always greyed out.
Any clue what I am doing wrong?
One addition: Is it possible to make the views in the childWindow FirstResponder without giving the childWindow key-status?
I got this working by mimicking the behaviour of NSPopover. On investigation I found that a popover (which uses a private NSPanel subclass "_NSPopoverWindow") believed it was the key & main window, even though it is not the window returned from [NSApp keyWindow].
Create your own custom NSPanel subclass, attach it to the parent window, and then override the following methods as so:
- (BOOL)isKeyWindow {
return YES;
}
- (BOOL)isMainWindow {
return YES;
}
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)canBecomeMainWindow {
return YES;
}
- (void)makeKeyWindow {
[super makeKeyWindow];
[self.parentWindow makeKeyWindow];
}
- (void)makeMainWindow {
[super makeMainWindow];
[self.parentWindow makeMainWindow];
}
- (void)becomeKeyWindow {
[super becomeKeyWindow];
}
- (void)becomeMainWindow {
[super becomeMainWindow];
[self.parentWindow becomeMainWindow];
}
- (void)resignMainWindow {
}
- (void)resignKeyWindow {
}

mouseDown: in a custom NSTextField inside an NSTableView

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

How does an NSView subclass communicate with the controller?

I am brand spanking new to Cocoa programming, and am still kind of confused about how things wire together.
I need a pretty simple application that will fire off a single command (let's call it DoStuff) whenever any point on the window is clicked. After a bit of research it looks like subclassing NSView is the right way to go. My ClickerView.m file has this:
- (void)mouseDown:(NSEvent *)theEvent {
NSLog(#"mouse down");
}
And I have added the View to the Window and have it stretching across the whole thing, and is properly writing to the log every time the window is clicked.
I also have my doStuff method on my controller (this could be refactored to its own class I suppose, but for now it works):
- (IBAction)doStuff:(id)sender {
// do stuff here
}
So, how do I get mouseDown in ClickerView to be able to call DoStuff in the controller? I have a strong .NET background and with that, I'd just have a custom event in the ClickerView that the Controller would consume; I just don't know how to do that in Cocoa.
edit based on Joshua Nozzi's advice
I added an IBOutlet to my View (and changed it to subclass NSControl):
#interface ClickerView : NSControl {
IBOutlet BoothController *controller;
}
#end
I wired my controller to it by clicking and dragging from the controller item in the Outlets panel on the View to the controller. My mouseDown method now looks like:
- (void)mouseDown:(NSEvent *)theEvent {
NSLog(#"mouse down");
[controller start:self];
}
But the controller isn't instantiated, the debugger lists it as 0x0, and the message isn't sent.
You could either add it as an IBOutlet like Joshua said, or you could use the delegate pattern.
You would create a Protocol that describes your delegate's methods like
#protocol MyViewDelegate
- (void)doStuff:(NSEvent *)event;
#end
then you'd make your view controller conform to the MyViewDelegate protocol
#interface MyViewController: NSViewController <MyViewDelegate> {
// your other ivars etc would go here
}
#end
Then you need to provide the implementation of the doStuff: in the implementation of MyViewController:
- (void)doStuff:(NSEvent *)event
{
NSLog(#"Do stuff delegate was called");
}
then in your view you'd add a weak property for the delegate. The delegate should be weak, so that a retain loop doesn't form.
#interface MyView: NSView
#property (readwrite, weak) id<MyViewDelegate> delegate;
#end
and then in your view you'd have something like this
- (void)mouseDown:(NSEvent *)event
{
// Do whatever you need to do
// Check that the delegate has been set, and this it implements the doStuff: message
if (delegate && [delegate respondsToSelector:#selector(doStuff:)]) {
[delegate doStuff:event];
}
}
and finally :) whenever your view controller creates the view, you need to set the delegate
...
MyView *view = [viewController view];
[view setDelegate:viewController];
...
Now whenever your view is clicked, the delegate in your view controller should be called.
First, your view needs a reference to the controller. This can be a simple iVar set at runtime or an outlet (designated by IBOutlet) connected at design time.
Second, NSControl is a subclass of NSView, which provides the target/action mechanism machinery for free. Use that for target/action style controls. This provides a simple way of setting the reference to your controller (the target) and the method to call when fired (the action). Even if you don't use a cell, you can still use target/action easily (NSControl usually just forwards this stuff along to its instance of an NSCell subclass but doesn't have to).
you can also use a selector calling method,
define two properties in custom class:
#property id parent;
#property SEL selector;
set them in view controller:
graph.selector=#selector(onCalcRate:);
graph.parent=self;
and call as:
-(void)mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
[_parent performSelector:_selector withObject:self];
}

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