Cocoa: Key down event on NSView not firing - cocoa

I have made a custom NSView and have implemented the keyDown: method. However, when I press keys the method is never called. Do I have to register to receive those events? fyi, I am making a document based application and can handle this code anywhere (doesn't have to be in this view). What is the best place to do this in a document based application such that the event will occur throughout the entire application?

You need to override -acceptsFirstResponder to return YES.

In Swift:
class MDView: NSView {
override var acceptsFirstResponder: Bool { return true }
}

Related

UI Save/Restoration mechanism in Cocoa via Swift

I'd like to save the state of Check Box, quit application, then launch macOS app again to see restored state of my Check Box. But there's no restored state in UI of my app.
What am I doing wrong?
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var tick: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(tick.state, forKey: "")
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let state = coder.decodeObject(forKey: "") as? NSControl.StateValue {
tick.state = state
}
}
}
To the best of my knowledge, this is the absolute minimum you need to implement custom UI state restoration of a window and/or its contents.
In this example, I have a window with a checkbox and that checkbox's state represents some custom view state that I want to restore when the app is relaunched.
The project contains a single window with a single checkbox button. The button's value is bound to the myState property of the window's content view controller. So, technically, the fact that this is a checkbox control is irrelevant; we're actually going to preserve and restore the myState property (the UI takes care of itself).
To make this work, the window's restorable property is set to true (in the window object inspector) and the window is assigned an identifier ("PersistentWindow"). NSWindow is subclassed (PersistentWindow) and the subclass implements the restorableStateKeyPaths property. This property lists the custom properties to be preserved/restored.
Note: if you can define your UI state restoration in terms of a list of key-value compliant property paths, that is (by far) the simplest solution. If not, you must implement encodeRestorableState / restoreState and are responsible for calling invalidateRestorableState.
Here's the custom window class:
class PersistentWindow: NSWindow {
// Custom subclass of window the perserves/restores UI state
// The simple way to preserve and restore state information is to just declare the key-value paths
// of the properties you want preserved/restored; Cocoa does the rest
override class var restorableStateKeyPaths: [String] {
return [ "self.contentViewController.myState" ]
}
// Alternatively, if you have complex UI state, you can implement these methods
// override func encodeRestorableState(with coder: NSCoder) {
// // optional method to encode special/complex view state here
// }
//
// override func restoreState(with coder: NSCoder) {
// // companion method to decode special/complex view state
// }
}
And here's the (relevant portion) of the content view controller
class ViewController: NSViewController {
#objc var myState : Bool = false
blah, blah, blah
}
(I built this as a Cocoa app project, which I could upload if someone tells me where I could upload it to.)
Actually you don't have to go through restorableStateKeyPaths / KVO / KVC if you don't want to.
I was stuck in the same state as you with the encodeRestorableState() & restoreState() methods not being called but found out what was missing.
In System Preferences > General, make sure "Close windows when quitting an app" is unchecked.
Make sure that the NSWindow containing your view has "Restorable" behavior enabled in IB.
Make sure that your NSViewController has a "Restoration ID" set.
Your NSViewController won't be encoded unless you call invalidateRestorableState(). You need to call this each time there's a state in your NSViewController that changes and that you want to have saved.
When no state changes in the NSViewController after having restored it, its state would not be encoded again when closing the app. Which would cause the custom states to not be restored when relaunching the app. The simplest way I found is to also call invalidateRestorableState() in viewDidLoad(), so that state is always saved.
After doing all that, I didn't even have to additionally implement NSApplicationDelegate or NSWindowRestoration protocol methods. So the state restoration of the NSViewController is pretty self-contained. Only external property is restorable NSWindow.
After losing a couple of hours of my life to this problem I finally got it working. Some of the information in the other answers was helpful, some was missing, some was not necessary.
Here is my minimal example based on a new Xcode 13 project:
in AppDelegate add (this is missing in the other examples):
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true }
in ViewController add:
#objc var myState : Bool = false
override class var restorableStateKeyPaths: [String] {
return [ "myState" ]
}
set up some UI and bind it to myState to see what is going on
make sure System Preferences > General > "Close windows when quitting an app" is unchecked
Things that I did not need to do:
create a custom window subclass
set a custom restoration id
it worked fine just with Xcode start/stop

How to close window (NSWindowController) by hitting the ESC key?

Issue
I would like the user being able to close a window by hitting the ESC key but I can't get it to work in this specific case, hitting ESC triggers an error sound (the "no you can't do that" macOS bloop) and nothing happens.
Context
I'm making a subclass of NSWindowController which itself creates an instance of a subclass of NSViewController and sets it in a view. Both controllers have their own xib file.
NSWindowController:
final class MyWindowController: NSWindowController, NSWindowDelegate {
#IBOutlet weak var targetView: MainView!
let myVC: MyViewController!
var params: SomeParams!
override func windowDidLoad() {
super.windowDidLoad()
myVC = MyViewController(someParams: params)
myVC.view.setFrameSize(targetView.frame.size)
myVC.view.setBoundsSize(targetView.bounds.size)
targetView.addSubview(myVC.view)
}
override var windowNibName: String! {
return "MyWindowController"
}
convenience init(someParams params: SomeType) {
self.init(window: nil)
self.params = params
}
}
NSViewController:
final class MyViewController: NSViewController {
convenience init(someParams params: SomeType) {
// do stuff with the params
}
override func viewDidLoad() {
super.viewDidLoad()
// configure stuff for the window
}
}
What I've tried
I suppose that my issue is that the MyWindowController NSWindow is the .initialFirstResponder when I would want the content of the targetView (an NSTableView) to be the first responder - this way I could use keyDown, I guess, and send the close command to the window from there. This doesn't seem optimal, though.
I've tried forcing the view controller views into being the first responder by using window?.makeFirstResponder(theView) in the windowDidLoad of MyWindowController but nothing ever changes.
I've also tried adding this to MyWindowController:
override func cancelOperation(_ sender: Any?) {
print("yeah, let's close!")
}
But this only works if the user clicks first on the background of the window then hits ESC, and it still emits the error sound anyway. Which is actually what made me think that the issue was about the first responder being on the window.
Question
How would you achieve that? Of course, I know that the user can already close the window with CMD+W, but I'd really like to sort out this issue nonetheless.
Note that the code example is in Swift but I can also accept explanations using Objective-C.
The documentation of cancelOperation explains how cancelOperation should work:
This method is bound to the Escape and Command-. (period) keys. The key window first searches the view hierarchy for a view whose key equivalent is Escape or Command-., whichever was entered. If none of these views handles the key equivalent, the window sends a default action message of cancelOperation: to the first responder and from there the message travels up the responder chain.
If no responder in the responder chain implements cancelOperation:, the key window searches the view hierarchy for a view whose key equivalent is Escape (note that this may be redundant if the original key equivalent was Escape). If no such responder is found, then a cancel: action message is sent to the first responder in the responder chain that implements it.
NSResponder declares but does not implement this method.
NSWindow implements cancelOperation: and the next responder, the window controller, isn't checked for an implementation of cancelOperation:. The cancel: message does arrive at the window controller. Implementing
- (void)cancel:(id)sender
{
NSLog(#"cancel");
}
will work. The cancel: message isn't inherited from a superclass so autocompletion doesn't suggest it.
This worked for me in Xcode 10 and Swift 4.2:
#objc func cancel(_ sender: Any?) {
close()
}
I tried it before but without the #objc part and it didn't work. So don't omit it.
When I needed such behavior I implemented it by overriding keyDown: of the NSWindow object.
I.e. something like the following:
- (void)keyDown:(NSEvent *)theEvent
{
int k = [theEvent keyCode];
if (k == kVK_Escape)
{
[self close];
return;
}
[super keyDown:theEvent];
}

Disable undo/redo in Cocoa app

I've implemented undo/redo the standard way (NSUndoManager) but can't figure out how I disable undo/redos when my app is in a specific state.
Users draw things in my app and when what they've drawn is uploading I disable the UI and of course don't want the user to be able to undo/redo.
I use a NSView's Undo Manager so I guess one way could be to just make that view resign first responder. Is there another way?
If the view is the first responder, you can implement the validateMenuItem: protocol to disable or enable the menu items according to your current state.
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
SEL action = menuItem.action;
if (action == #selector(undo:) ||
action == #selector(redo:)) {
return !uploadingImage;
}
return YES;
}
You can finalize undo and redo with
- (void) removeAllActions;
or remove actions for a specific target with
- (void) removeAllActionsWithTarget: (id) target;
If you simply want to disable any actions for a time, leaving the undo stack unchanged, simply disable the Undo/Redo menu items using NSMenuValidationProtocol's
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem;
The best approach I can think of is making the view's -undoManager method return nil during uploads, which will remove it from the responder chain and cause undo/redo options to be disabled for that view.
(I haven't tested this, but I'm 99% sure that the menus will ask your view for the undo manager whenever it validates the menu options.)
I had a similar situation where I wanted to conditionally disable certain undo/redo operations when the app is in a specific state (while still allowing undo/redo for other operations).
The method of implementing - (BOOL)validateMenuItem:(NSMenuItem *)item on a view doesn't work for me (I have a document-based app on 10.12). Per the docs at https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MenuList/Articles/EnablingMenuItems.html:
If there is an object in the responder chain that implements the item’s action, NSMenu then checks to see if that object implements the validateMenuItem: or validateUserInterfaceItem: method. If it does not, then the menu item is enabled. If it does, then the enabled status of the menu item is determined by the return value of the method.
The view would have to add an undo method the does the right thing as well.
When I probed the responder chain, I found that my NSWindow was the object that responded to undo: (though it's not part of the documented interface), so my current plan is to use a custom NSWindow subclass with the imeplementation of validateMenuItem, along the lines of:
#import "Window.h"
#implementation SBXWindow
- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag screen:(NSScreen *)screen
{
self = [super initWithContentRect:contentRect styleMask:style backing:bufferingType defer:flag screen:screen];
return self;
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
// Call super imeplementation as it appears to update the menu item title (and potentially other stuff)
BOOL result = [super validateMenuItem:item];
if (result == NO) {
return NO;
}
if (item.action == #selector(undo:) || item.action == #selector(redo:)) {
// Add custom logic here
}
return result;
}
#end
However there are warnings that the undo: redo: methods aren't implemented. These can be eliminated by creating a category on NSWindow, such as:
#interface NSWindow (SBXUndoable)
- (void)undo:(id)sender;
- (void)redo:(id)sender;
#end
Not sure if there are any issues with doing that (I didn't notice any), but it does eliminate the warnings. I've since changed the class to a Swift class, which didn't have any warnings to deal with.
The documentation is your friend. The disableUndoRegistration method of NSUndoManager has "disable" in its name. It's up to your app's controllers to decide when it's appropriate to disable and re-enable undo registration.

How to handle keyboard events in subclass of NSViewController?

I am creating one cocoa application having wizard like structure.
All dialogs are subclass of NSViewController. Currently I am not able get keyboard events such as keyDown and keyUp..
Please help me to solve this problem....
Thanks in advance....
Override keyDown: and keyUp: method.
-(void)keyUp:(NSEvent*)event
-(void)keyDown:(NSEvent*)event
and
- (BOOL)acceptsFirstResponder {
return YES;
}
In subclass of NSViewController
you should refer Cocoa Event-Handling Guide .
If you are trying to simply get an event for escape, use this instead:
override var acceptsFirstResponder: Bool {
return true
}
override func cancelOperation(_ sender: Any?) {
// The user pressed escape
}
Setting window to "Auto Recalculates View Loop" in Inteface Builder has worked for me.

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