How to capture escape key in an NSWindow? - macos

I've got a storyboard with an NSWindowController (the storyboard entry point), whose content is an NSViewController. When the user selects a menuitem in my application, I load the storyboard and display it.
Now I want the "escape" key to close the window. I've seen several questions about this, but none that matches the behavior I'm seeing.
I subclassed NSWindowController and NSViewController, and set the controllers in the storyboard to those. I've tried every method I could find or think of, including:
override func cancelOperation(_ sender: Any?)
func cancel(_ sender: Any?)
override func keyDown(with event: NSEvent)
in the window controller, the view controller, and even a custom NSWindow subclass, but none of these methods get called. I've confirmed that viewDidLoad() and windowDidLoad() do get called, so my classes are getting used. They're just not getting events.
To verify my sanity, I tried inspecting the responder chain. As expected, both my custom window controller and view controller classes are in it, as is the NSWindow subclass.
Why would an NSResponder object in the responder chain not receive events?

// monitor ESC key
NSEvent* (^handler)(NSEvent*) = ^(NSEvent *event) {
if(event.keyCode == 53) {
[self keyDown:event];
}
return event;
};
eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:handler];
Put these code into a NSWindowController or AppDelegate class.

Related

NSTableCellView inside a custom view refuses first responder

Instead of a TableView I want to use a NSTableCellView inside a custom view. So I created a xib file with a standard NSTableCellView and loaded that into a view.
The table cell view is displayed as expected. But I can't make its textfield the first responder. It's not responding to mouse events and it's not even reacting when I explicitly make it the first responder. makeFirstResponder returns true, but I see no blinking cursor and it doesn't respond to any key events.
Adding a regular textfield to the view does work however.
class ViewController: NSViewController
{
#IBOutlet var myView: NSView!
#IBOutlet var cellView: NSTableCellView! //ViewController is the file owner of the xib file
override func viewDidLoad()
{
super.viewDidLoad()
NSBundle(forClass: ViewController.self).loadNibNamed("Cell", owner: self, topLevelObjects: nil)
myview.addSubview(cellView)
cellView.layer?.borderColor = Color.blackColor().CGColor
cellView.layer?.borderWidth = 1
}
override func mouseDown(event: NSEvent)
{
let result = self.view.window?.makeFirstResponder(cellView.textField!)
print(result) //prints true
}
Why am I doing this? Well, I am trying to understand how a view-based table view would be implemented. It seems that at its core a tableview is just a view with lots of (probably cached and reused?) subviews. Also, I always thought that the number of views was limited for efficiency purposes on OS X?

self.window is always nil

I'm currently trying to display a window with a window controller.
That is what I have:
NSWindow subclass
import Cocoa
import CoreLocation
class TweetWindow: NSWindow {
var locationManager: CLLocationManager!
var geoCoder: CLGeocoder!
#IBAction func tweetButtonPressed(sender:NSButton) {
}
func initialize() {
self.titleVisibility = NSWindowTitleVisibility.Hidden;
self.locationManager = CLLocationManager();
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
self.locationManager.distanceFilter = 10;
self.geoCoder = CLGeocoder();
}
func windowWillShow() {
if !self.visible {
let systemAppearanceName = (NSUserDefaults.standardUserDefaults().stringForKey("AppleInterfaceStyle") ?? "Light").lowercaseString;
let systemAppearance = systemAppearanceName == "dark" ? NSAppearance(named: NSAppearanceNameVibrantDark) : NSAppearance(named: NSAppearanceNameVibrantLight);
self.appearance = systemAppearance;
self.locationManager.startUpdatingLocation();
}
}
func windowWillClose() {
self.locationManager.stopUpdatingLocation();
}
}
NSWindowController subclass:
import Cocoa
class TweetWindowController: NSWindowController {
var tweetWindow: TweetWindow { return self.window as! TweetWindow; }
override func windowDidLoad() {
super.windowDidLoad()
self.tweetWindow.initialize()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}
override func showWindow(sender: AnyObject?) {
self.tweetWindow.windowWillShow()
super.showWindow(sender)
}
}
Of course, I've got a .xib-file, too, that contains my window. It is called "TweetWindow.xib".
Now, so far this should be ok.
In my AppDelegate.swift I do the following:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var tweetWindowController:TweetWindowController!;
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
tweetWindowController = TweetWindowController(windowNibName: "TweetWindow");
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
func showTweetWindowInternal() {
NSApp.activateIgnoringOtherApps(true);
tweetWindowController.showWindow(nil);
}
#IBAction func showTweetWindow(sender: AnyObject) {
showTweetWindowInternal();
}
#IBAction func quitApp(sender: AnyObject) {
NSApplication.sharedApplication().terminate(self);
}
}
My problem is the following:
When I try to click on my button that is associated with the IBAction down there to show the window, an exception is thrown here:
var tweetWindow: TweetWindow { return self.window as! TweetWindow; }
It says fatal error: unexpectedly found nil while unwrapping an Optional value, so window is nil.
Why is window nil there? Am I trying to access the value too early or something?
Here are some photos:
Thanks.
Initializing an instance of NSWindowController or a subclass does not load the NIB. The NIB is not loaded until the window property is accessed or the showWindow() method is called (which basically accesses the window property indirectly). In your case, since you're overriding showWindow(), it's important to know that the NIB is not loaded until the superclass implementation is called.
So, yes, your call to self.tweetWindow.windowWillShow() in your showWindow() override, before calling through to super, is too early. The NIB has not been loaded at that point, so the window outlet has not been connected to anything.
Of course, you have to make sure the outlet is actually connected in the NIB or it will never be connected, even after the NIB is loaded. But trying to access it before it's loaded is the first problem.
I think your windowWillShow() method is misguided, at least as implemented. The window can be shown in various ways, not just by the window controller's showWindow() method. For example, something outside of both the window and the window controller could do tweetWindowController.window.makeKeyAndOrderFront(nil). If you really want to do something like this, have the window class override the various order...() methods to see if the window is being ordered in for the first time and, if so, call your method.
Update:
You have several things misconfigured in your NIB. Here's what you need to do to fix them:
Break the current connection from the "delegate" outlet of File's Owner to the window. Click the "x" button seen in either of the screenshots you posted.
Change the class of File's Owner. It is currently TweetWindow. It should be TweetWindowController. The controller is what loads and owns the NIB, so the class of File's Owner should be the controller class.
Connect the "window" outlet of File's Owner to the window.
Connect the "delegate" outlet of the window to File's Owner.

how to presentViewControllerAsSheet on OSX Mavericks?

It's a long story, but to cut it short; my first OSX app was written (on Yosemite) in Swift using a storyboard until I found out my (finished) app will not run on Mavericks. I need to run on Mavericks, so I have replaced the storyboard with NIBs.
My problem is with the segues; I was using 'sheet type' segues to show other view controllers in a sheet over the main view controller. A call to the presentViewControllerAsSheet method of NSViewController is a good replacement as it looks the same, but this API was introduced in Yosemite - so I need to work out how to do this for Mavericks.
In the action for a button on the main view, I've tried using beginSheet like this:
secondViewController = SecondViewController(nibName: "SecondViewController", bundle: nil)
self.view.window?.beginSheet(secondViewController!view.window!, completionHandler: nil)
But the second view controller's window is null at runtime. I've tried adding the new view controller as a subview to the application window but this is an unrecognised selector:
NSApplication.sharedApplication().windows[0].addSubView(secondViewController!.view)
I've search high and low for a description of how to show a sheet and all I can find is: Can a view controller own a sheet? but I'm sorry to admit I don't understand the answer. Can anybody help me with some working code? I'm beginning to worry that I'm trying to do something unusual but it looks OK on Yosemite, so how did people do this before Yosemite was released?
EDIT
I still haven't got to the solution, so I have put together a small app which shows the problems I'm having.
In AppDelegate.swift:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mainViewController: FirstView!
func applicationDidFinishLaunching(aNotification: NSNotification) {
mainViewController = FirstView(nibName:"FirstView", bundle: nil)
window.contentView = mainViewController.view
mainViewController.view.frame = (window.contentView as! NSView).bounds
}
}
In FirstView.swift (associated NIB has a 'open sheet' button)
class FirstView: NSViewController {
var secondView: SecondView?
var secondWindow: SecondWinCon?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func pressButton(sender: AnyObject) {
secondView = SecondView(nibName: "SecondView", bundle: nil)!
// method 1 - this is the behaviour I want (but it only works on OSX 10.10)
// presentViewControllerAsSheet(secondView!)
// method 2 - this just creates a floating window
// self.view.addSubview(secondView!.view)
// self.view.window?.beginSheet(secondView!.view.window!, completionHandler: nil)
// method 3 - this also creates a floating window
secondWindow = SecondWinCon(windowNibName: "SecondWinCon")
self.view.window?.beginSheet(secondWindow!.window!, completionHandler: nil)
}
}
In SecondView.swift (associated NIB has a 'close' button)
class SecondView: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismissPressed(sender: AnyObject) {
if (presentingViewController != nil) {
presentingViewController?.dismissViewController(self)
} else {
self.view.window?.sheetParent?.endSheet(self.view.window!)
}
}
}
In SecondWinCon.swift (Associated NIB is empty)
class SecondWinCon: NSWindowController {
var secondView: SecondView?
override func windowDidLoad() {
super.windowDidLoad()
secondView = SecondView(nibName: "SecondView", bundle: nil)!
self.window?.contentView.addSubview(secondView!.view)
}
}
If method 1 is uncommented, you will see the behaviour I'm trying to emulate (remember it only works on OS X 10.10). Method 2 or 3 displays the second view, but not as a sheet.
I have the same problem, and found maybe is't an issue related to view life cycle.
When I call presentViewControllerAsSheet in viewDidLoad, sheet will not shown, and you will get this in console:
Failed to set (contentViewController) user defined inspected property on (NSWindow): presentViewController:animator:: View '''s view is not in a window/view hierarchy.
If you trigger this in viewWillAppear or viewDidAppear, it's totally no problem.
UPDATE
Okay, let's make it clear.
For this initial storyboard, NSWindowController is connected with a view controller, think this as a root view controller (RootVC).
Create another view controller desired as a sheet in storyboard (SheetVC).
in viewWillAppear or viewDidAppear of RootVC, [self presentViewControllerAsSheet: SheetVC]
The sheet will show, no additional code required.
If you get here looking for a solution, I was nearly there with method 3. The important step I had missed was to turn off "Visible At Launch" in the NSWindowController's NIB (it's an attribute of the NSWindow). In my sample code, this was in SecondWinCon.nib.

Detecting when a button is touched in SKScene?

I created a storyboard and added a scene with two buttons. I cannot figure out how to know when a button is pressed on my GameScene.swift class.
How can this be done?
You can you touchesBegan for that.
Here is example code for you:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches{
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.playButton{
//your code
}
}
}
You appear to be mixing UIKit and SpriteKit here. I would personally advise against using UIButtons in conjunction with Sprite Kit. Is there a specific reason for doing so?
There are two ways you can implement button behavior within a Sprite Kit scene:
have the SKScene object handle the touches
have the button itself handle the touches
Dharmesh's answer uses method (1), where he implements the -touchesBegan method.
In my current project, I am using an SKNode subclass as a button (2). I am unfamiliar with Swift syntax so I have posted Objective-C code from my project instead. The method calls are similar though and should help illustrate the point.
If you want an SKNode to receive touches, set userInteractionEnabled to YES. Otherwise, the closest ancestor with userInteractionEnabled = YES (which typically is the containing SKScene) will receive a -touchesBegan/-touchesMoved/-touchesEnded message.
#interface VTObject : SKNode
#end
...
#implementation VTObject
- (instancetype)init {
if (self = [super init]) {
self.userInteractionEnabled = YES;
}
return self;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"button touched!");
}
#end
You should add the UIButton programatically, instead of in IB, to the SKScene's SKView (in didMoveToView for example). You can then set the target for the button with button.addTarget:action:forControlEvents:. Just remember to call button.removeFromSuperview() in willMoveFromView otherwise you'll see the buttons in your next scene.

Text change notification for an NSTextField

I would like to use the code from the answer to this question: How to observe the value of an NSTextField on an NSTextField in order to observe changes on the string stored in the NSTextField.
[[NSNotificationCenter defaultCenter]
addObserverForName:NSTextViewDidChangeSelectionNotification
object:self.textView
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note){
NSLog(#"Text: %#", self.textView.textStorage.string);
}];
The class used here is an NSTextView. I can't find a notification in NSTextField to use instead of NSTextViewDidChangeSelectionNotification.
Is there a notification available in NSTextField that can be used in this case ?
If you just want to detect when the value of a text field has changed, you can use the controlTextDidChange: delegate method that NSTextField inherits from NSControl.
Just connect the delegate outlet of the NSTextField in the nib file to your controller class, and implement something like this:
- (void)controlTextDidChange:(NSNotification *)notification {
NSTextField *textField = [notification object];
NSLog(#"controlTextDidChange: stringValue == %#", [textField stringValue]);
}
If you're creating the NSTextField programmatically, you can use NSTextField's setDelegate: method after creation to specify the delegate:
NSTextField *textField = [[[NSTextField alloc] initWithFrame:someRect] autorelease];
[textField setDelegate:self]; // or whatever object you want
Delegation is one of the fundamental design patterns used throughout Cocoa. Briefly, it allows you to easily customize the behavior of standard objects (in this case, user interface objects) without the complexity involved in having to subclass the object to add that additional behavior. For example, another lower-level way to detect when the text in a textfield has changed might be to create your own custom NSTextField subclass in which you override the keyDown: method that NSTextField inherits from NSResponder. However, subclassing like that is difficult because it can require that you have an intimate knowledge of the object's inheritance hierarchy. For more info, definitely check out the following:
Cocoa Fundamentals Guide: Delegates and Data Sources
Regarding what id <NSTextFieldDelegate> means: it means a generic object (id) that declares itself as conforming to the <NSTextFieldDelegate> protocol. For more info on protocols, see The Objective-C Programming Language: Protocols.
Sample GitHub project at: https://github.com/NSGod/MDControlTextDidChange
Xcode 9.2. with Swift 4.0.3.
The NSTextField must be connected via interface builder for this implementation to work.
import Cocoa
#objc public class MyWindowController: NSWindowController, NSTextFieldDelegate {
#IBOutlet weak var myTextField: NSTextField!
// MARK: - ViewController lifecycle -
override public func windowDidLoad() {
super.windowDidLoad()
myTextField.delegate = self
}
// MARK: - NSTextFieldDelegate -
public override func controlTextDidChange(_ obj: Notification) {
// check the identifier to be sure you have the correct textfield if more are used
if let textField = obj.object as? NSTextField, self.myTextField.identifier == textField.identifier {
print("\n\nMy own textField = \(self.myTextField)\nNotification textfield = \(textField)")
print("\nChanged text = \(textField.stringValue)\n")
}
}
}
Console output:
My own textField = Optional(<myApp.NSTextField 0x103f1e720>)
Notification textfield = <myApp.NSTextField: 0x103f1e720>
Changed text = asdasdasddsada
You should use NSTextFieldDelegate and implement controlTextDidChange. Test in macOS 10.14 and Swift 4.2
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate {
#IBOutlet weak var textField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
}
func controlTextDidChange(_ obj: Notification) {
let textField = obj.object as! NSTextField
print(textField.stringValue)
}
}
I believe you want to read up on the field editor which is essentially a (hidden) NSTextView that handles the text input to all the NSTextFields in a given window. The section on "Using Delegation and Notification With the Field Editor" should point you in the right direction.
In Swift it's
public override func controlTextDidChange(_ obj: Notification) {
}

Resources