NSOpenGLView, NSWindow & NSResponder - makeFirstResponder not working - cocoa

In the code below I am Initialising a NSViewController [a NSResponder], with a NSWindow, a NSOpenGLView, presenting the view and attempting to set the NSViewController as the windows first responder.
It is not working. I was expecting to be able to hit a breakpoint in the keyUp: and keyDown: methods also below, but nothing is happening.
Am I missing something?
-(void)initwithFrame:(CGRect)frame
{
window = [[MyNSWindow alloc] initWithContentRect:frame styleMask:NSClosableWindowMask | NSTitledWindowMask backing:NSBackingStoreBuffered defer: YES ];
OpenGLView* glView = [[[OpenGLView alloc] initWithFrame:window.frame] autorelease];
window.contentView = glView;
[window makeFirstResponder:self];
[window makeKeyWindow];
[window display];
}
-(void)keyDown:(NSEvent*)theEvent
{
unichar unicodeKey = [ [ theEvent characters ] characterAtIndex:0 ];
unicodeKey = 0;
}
-(void)keyUp:(NSEvent *)theEvent
{
unichar unicodeKey = [ [ theEvent characters ] characterAtIndex:0 ];
unicodeKey = 0;
}

Returning to this issue, actually the problem is elsewhere.
I had been using this sleep function to control the apps frame rate:
void System::Sleep(double seconds)
{
NSDate* limit = [NSDate dateWithTimeIntervalSinceNow:seconds];
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
[runLoop runUntilDate:limit];
}
Doing this seems to freeze the system completely and block the key events.
Instead use this to schedule the update function:
[NSTimer scheduledTimerWithTimeInterval:interval target:self selector:#selector(updateApp:) userInfo:nil repeats:YES];

For instances to participate in key-view loops, a custom view must return YES from acceptsFirstResponder.

I had this problem also. This thread might help
keyDown not being called
I found out what was preventing the keyDown event from being called.
It was the NSBorderlessWindowMask mask, it prevents the window from
become the key and main window. So I have created a subclass of
NSWindow called BorderlessWindow:
#interface BorderlessWindow : NSWindow { }
#end
#implementation BorderlessWindow
- (BOOL)canBecomeKeyWindow {
return YES; }
- (BOOL)canBecomeMainWindow {
return YES; }
#end

Related

ipad: predictive search in a popover

I want to implement this
1) when user start typing in a textfield a popOver flashes and shows the list of items in a table view in the popover as per the string entered in textfield.
2) Moreover this data should be refreshed with every new letter entered.
kind of predictive search.
Please help me with this and suggest possible ways to implement this.
UISearchDisplayController does most of the heavy lifting for you.
Place a UISearchBar (not a UITextField) in your view, and wire up a UISearchDisplayController to it.
// ProductViewController.h
#property IBOutlet UISearchBar *searchBar;
#property ProductSearchController *searchController;
// ProductViewController.m
- (void) viewDidLoad
{
[super viewDidLoad];
searchBar.placeholder = #"Search products";
searchBar.showsCancelButton = YES;
self.searchController = [[[ProductSearchController alloc]
initWithSearchBar:searchBar
contentsController:self] autorelease];
}
I usually subclass UISearchDisplayController and have it be it's own delegate, searchResultsDataSource and searchResultsDelegate. The latter two manage the result table in the normal manner.
// ProductSearchController.h
#interface ProductSearchController : UISearchDisplayController
<UISearchDisplayDelegate, UITableViewDelegate, UITableViewDataSource>
// ProductSearchController.m
- (id)initWithSearchBar:(UISearchBar *)searchBar
contentsController:(UIViewController *)viewController
{
self = [super initWithSearchBar:searchBar contentsController:viewController];
self.contents = [[NSMutableArray new] autorelease];
self.delegate = self;
self.searchResultsDataSource = self;
self.searchResultsDelegate = self;
return self;
}
Each keypress in the searchbar calls searchDisplayController:shouldReloadTableForSearchString:. A quick search can be implemented directly here.
- (BOOL) searchDisplayController:(UISearchDisplayController*)controller
shouldReloadTableForSearchString:(NSString*)searchString
{
// perform search and update self.contents (on main thread)
return YES;
}
If your search might take some time, do it in the background with NSOperationQueue. In my example, ProductSearchOperation will call showSearchResult: when and if it completes.
// ProductSearchController.h
#property INSOperationQueue *searchQueue;
// ProductSearchController.m
- (BOOL) searchDisplayController:(UISearchDisplayController*)controller
shouldReloadTableForSearchString:(NSString*)searchString
{
if (!searchQueue) {
self.searchQueue = [[NSOperationQueue new] autorelease];
searchQueue.maxConcurrentOperationCount = 1;
}
[searchQueue cancelAllOperations];
NSInvocationOperation *op = [[[ProductSearchOperation alloc]
initWithController:self
searchTerm:searchString] autorelease];
[searchQueue addOperation:op];
return NO;
}
- (void) showSearchResult:(NSMutableArray*)result
{
self.contents = result;
[self.searchResultsTableView
performSelectorOnMainThread:#selector(reloadData)
withObject:nil waitUntilDone:NO];
}
It sounds like you have a pretty good idea of an implementation already. My suggestion would be to present a UITableView in a popover with the search bar at the top, then simply drive the table view's data source using the search term and call reloadData on the table view every time the user types into the box.

Accessing Button in NIB that is in my framework

I am trying to create a simple framework with a nib that has a button on it which can be customized (selector wise and title wise.) for this, i did the following:
I added a property:
#property (nonatomic,retain) NSButton*accessoryButton;
and connected it to my outlet:
#synthesize accessoryButton = littleButton;
I then shared the instance as such:
+ (TestWindow *)sharedPanel
{
return sharedPanel ? sharedPanel : [[[self alloc] init] autorelease];
}
- (id)init
{
if (sharedPanel) {
[self dealloc];
} else {
sharedPanel = [super init];
}
return sharedPanel;
}
and load the nib:
if( !somewindow )
{
[NSBundle loadNibNamed: #"window" owner:nil];
}
[NSApp activateIgnoringOtherApps:YES];
[somewindow center];
[somewindow setLevel:NSModalPanelWindowLevel];
[somewindow makeKeyAndOrderFront:self];
When I however want to change the title for example from my sample project, it never works.
[TestWindow sharedPanel] setTitle:#"hi"]; //doesnt work
Here's my setTitle: method:
-(void)setTitle:(NSString *)buttonTitle
{
[[self accessoryButton] setTitle:buttonTitle];
[[self accessoryButton] display];
}
I don't get an error but nothing happens either. What am I missing?
Is the button nil at runtime? Are you sure your button's outlet is connected?
Does your init function get called when the NIB is loaded?

NSTextField not noticing lost focus when pressing Tab?

I can't seem to find a way to get notified when an NSTextField loses focus by pressing the Tab key. I get a nice textDidEndEditing when clicking another control or when pressing Enter, but not if I change the focus by pressing the Tab key.
Also tried to yank KeyDown and doCommandBySelector for this purpose but I got nowhere.
Any ideas?
Thanks in advance
Edit:
Forgot to mention, but I tried resignFirstResponder too. This is the code I tried:
- (BOOL)resignFirstResponder
{
NSRunAlertPanel(#"", #"Lost Focus",#"OK", nil, nil);
return [super resignFirstResponder];
}
- (BOOL)becomeFirstResponder
{
NSRunAlertPanel(#"", #"Got focus",#"OK", nil, nil);
return [super becomeFirstResponder];
}
Strangely, what happens here is that when getting focus, both becomeFirstResponder and resignFirstResponder are called one after the other. But when changing focus away from the control, neither are.
"I get a nice textDidEndEditing when
clicking another control or when
pressing Enter, but not if I change
the focus by pressing the Tab key."
As of April 2011, with OS X 10.6 libs, I'm using:
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
...to listen for NSTextField losing focus, and it's working correctly. Is this possible in your situation? Is it something that used to be broken, but is now fixed by Apple?
If so, it's much less code :).
Ok, I've found a way to do it: use a window delegate to make the window return a custom field editor. This field editor keeps track of the last TextField that's been activated and calls its textDidEndEditting method when losing firstResponder itself. Here's an example of how to do it:
#import <Cocoa/Cocoa.h>
#import <AppKit/AppKit.h>
#interface MyTextField : NSTextField
- (BOOL)resignFirstResponder;
- (void)textDidEndEditing:(NSNotification *)notification;
#end
#interface MyFieldEditor : NSTextView
{
MyTextField * lastBox;
}
-(void) setLastEditBox:(MyTextField*) box;
#end
#interface MyWindowDelegate : NSWindowController
{
MyFieldEditor *fieldEditor;
}
#end
#implementation MyFieldEditor
-(void) setLastEditBox:(MyTextField*) box{ lastBox = box; }
-(id)init
{
if (self = [super init])
[self setFieldEditor:YES];
return self;
}
- (BOOL)resignFirstResponder
{
// Activate the last active editbox editting-end event
if(lastBox != nil)
{
[lastBox textShouldEndEditing:self];
lastBox = nil;
}
return [super resignFirstResponder];
}
#end
#implementation MyWindowDelegate
-(id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client
{
if(fieldEditor == nil) // Return our special field editor
fieldEditor = [[[MyFieldEditor alloc] autorelease] init];
return fieldEditor;
}
#end
#implementation MyTextField
- (BOOL)resignFirstResponder
{
// We're losing first responder, inform the field editor that this was the last edit box activated
MyFieldEditor* myTf = (MyFieldEditor*) [[self window] fieldEditor:YES forObject:self];
[myTf setLastEditBox:self];
return [super resignFirstResponder];
}
- (void)textDidEndEditing:(NSNotification *)notification;
{
[super textDidEndEditing:notification];
[self setStringValue:#"RECEIVED ENDEDITING"];
}
#end
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSApplication *app = [NSApplication sharedApplication];
NSRect frame = NSMakeRect(100, 100, 200, 150);
// Create the window
NSWindow* window = [[[NSWindow alloc] autorelease ] initWithContentRect:frame styleMask:NSClosableWindowMask|NSResizableWindowMask
backing:NSBackingStoreBuffered defer:NO];
[window setDelegate:[[MyWindowDelegate alloc] autorelease]];
MyTextField * tf = [ [[ MyTextField alloc ] autorelease] initWithFrame: NSMakeRect( 30.0, 100.0, 150.0, 22.0 ) ];
[ [ window contentView ] addSubview: tf ];
MyTextField * tf2 = [ [[ MyTextField alloc ] autorelease] initWithFrame: NSMakeRect( 30.0, 40.0, 150.0, 22.0 ) ];
[ [ window contentView ] addSubview: tf2 ];
[window makeKeyAndOrderFront: window];
[app run];
[pool release];
return 0;
}
You have to do only this
For key Tab
self.textfield.delegate = self;
and then implement this method
- (void)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
NSLog(#"Selector method is (%#)", NSStringFromSelector( commandSelector ) );
if (commandSelector == #selector(insertTab:)) {
//Do something against TAB key
//Or Call a Method
}
}
or see my answer at
Execute an Action when the Enter-Key is pressed in a NSTextField?
With the understanding that I mentioned in my other post, I figured out an answer. It's a little convoluted but it works. You have to subclass both the NSTextField and the NSWindow because you need information from both to set this up. Here's the subclasses:
HMTextField.h
#import <Foundation/Foundation.h>
#interface HMTextField : NSTextField {
}
#end
HMTextField.m
#import "HMTextField.h"
#import "HMWindow.h"
#implementation HMTextField
- (BOOL)becomeFirstResponder {
[(HMWindow*)[self window] setTfBecameFirstResponder:YES];
return [super becomeFirstResponder];
}
#end
HMWindow.h
#import <Foundation/Foundation.h>
#interface HMWindow : NSWindow {
BOOL tfIsFirstResponder, tfBecameFirstResponder;
}
#property (nonatomic, readwrite, assign) BOOL tfBecameFirstResponder;
#end
HMWindow.m
#import "HMWindow.h"
#implementation HMWindow
#synthesize tfBecameFirstResponder;
-(id)init {
if (self = [super init]) {
tfIsFirstResponder = NO;
}
return self;
}
- (NSResponder *)firstResponder {
id fr = [super firstResponder];
if ([fr isEqualTo:[self fieldEditor:NO forObject:nil]]) {
tfIsFirstResponder = YES;
} else {
if (tfIsFirstResponder && tfBecameFirstResponder) {
NSLog(#"the text field stopped being first responder");
tfBecameFirstResponder = NO;
}
tfIsFirstResponder = NO;
}
return fr;
}
#end
Make the classes and make your objects their class. You'll be notified of the first responder change from your text field where the NSLog message is in the HMWindow.m file. If you need help understanding how it works let me know.
Here's an example of how to indicate the appropriate time a custom NSTextFieldCell (NSCell) should draw its own bezel & focus ring (in the method [NSTextFieldCell drawWithFrame: inView]), by 'borrowing' the cell's highlight field, setting it when the text field gains focus, and clearing it when the text field loses focus (editing completes).
This technique overcomes some problems:
The cell can't easily determine if it has focus.
The cell can't easily determine which higher level component (e.g. text field or button) it belongs to to track via its parent
NSTextField can instantaneously resign first responder after gaining it, which could make it seem like it lost user focus when it didn't.
Since we're re-purposing the cell's "highlighted" state field, in order to communicate the focus state to the cell, be sure to return nil from the custom NSTextFieldCell's [highlightColorWithFrame: inView:] method.
#import "CustomTextField.h"
#implementation CustomTextField
-(BOOL)becomeFirstResponder {
((NSTextFieldCell *)self.cell).highlighted = true;
return [super becomeFirstResponder];
}
-(void)textDidEndEditing:(NSNotification *)notification {
((NSTextFieldCell *)self.cell).highlighted = false;
[super textDidEndEditing:notification];
}
#end
Complex answers. There is a simpler way to do it.
Don't forget to subclass your NSTextField to NotificableTextField and set its delegate to your view controller.
NotificableTextField.h:
#import <Cocoa/Cocoa.h>
#protocol NotificableTextFieldDelegate <NSObject>
#optional
- (void)textFieldStartedEditing:(NSTextField *)textField;
- (void)textFieldEndedEditing:(NSTextField *)textField;
#end
#interface NotificableTextField : NSTextField
#end
NotificableTextField.m:
#import "NotificableTextField.h"
#implementation NotificableTextField
- (void)awakeFromNib
{
[super awakeFromNib];
self.target = self;
self.action = #selector(inputEnd);
}
- (BOOL)becomeFirstResponder
{
BOOL status = [super becomeFirstResponder];
if (status && [self.delegate respondsToSelector:#selector(textFieldStartedEditing:)])
[(id<NotificableTextFieldDelegate>)self.delegate textFieldStartedEditing:self];
return status;
}
- (void)inputEnd
{
if ([self.delegate respondsToSelector:#selector(textFieldEndedEditing:)])
[(id<NotificableTextFieldDelegate>)self.delegate textFieldEndedEditing:self];
}
#end
NSTextField is a subclass of NSResponder. NSResponder has a method - (BOOL)resignFirstResponder. That will notify you when the NSTextField is no longer first responder... ie. loses focus. So subclass your NSTextField and do your stuff in there.

Cocoa nextEventMatchingMask not receiving NSMouseMoved event

I created a local event loop and showed up a borderless window (derived from NSPanel),
I found in the event loop there's no NSMouseMoved event received, although I can receive Mouse button down/up events.
What should I do to get the NSMouseMoved events? I found making the float window as key window can receive the NSMouseMoved events, but I don't want to change key window. And it appears this is possible, because I found after clicking the test App Icon in System Dock Bar, I can receive the mousemoved events, and the key window/mainwindow are unchanged.
Here's the my test code: (Create a Cocoa App project names FloatWindowTest, and put a button to link with the onClick: IBAction).
Thanks in advance!
-Jonny
#import "FloatWindowTestAppDelegate.h"
#interface FloatWindow : NSPanel
#end
#interface FloatWindowContentView : NSView
#end
#implementation FloatWindowTestAppDelegate
#synthesize window;
- (void)delayedAction:(id)sender
{
// What does this function do?
// 1. create a float window
// 2. create a local event loop
// 3. print out the events got from nextEventMatchingMask.
// 4. send it to float window.
// What is the problem?
// In local event loop, althrough the event mask has set NSMouseMovedMask
// there's no mouse moved messages received.
//
FloatWindow* floatWindow = [[FloatWindow alloc] init];
NSEvent* event = [NSApp currentEvent];
NSPoint screenOrigin = [[self window] convertBaseToScreen:[event locationInWindow]];
[floatWindow setFrameTopLeftPoint:screenOrigin];
[floatWindow orderFront:nil];
//Making the float window as Key window will work, however
//change active window is not anticipated.
//[floatWindow makeKeyAndOrderFront:nil];
BOOL done = NO;
while (!done)
{
NSAutoreleasePool* pool = [NSAutoreleasePool new];
NSUInteger eventMask = NSLeftMouseDownMask|
NSLeftMouseUpMask|
NSMouseMovedMask|
NSMouseEnteredMask|
NSMouseExitedMask|
NSLeftMouseDraggedMask;
NSEvent* event = [NSApp nextEventMatchingMask:eventMask
untilDate:[NSDate distantFuture]
inMode:NSDefaultRunLoopMode
dequeue:YES];
//why I cannot get NSMouseMoved event??
NSLog(#"new event %#", [event description]);
[floatWindow sendEvent:event];
[pool drain];
}
[floatWindow release];
return;
}
-(IBAction)onClick:(id)sender
{
//Tried to postpone the local event loop
//after return from button's mouse tracking loop.
//but not fixes this problem.
[[NSRunLoop currentRunLoop]
performSelector:#selector(delayedAction:)
target:self
argument:nil
order:0
modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
}
#end
#implementation FloatWindow
- (id)init
{
NSRect contentRect = NSMakeRect(200,300,200,300);
self = [super initWithContentRect:contentRect
styleMask:NSTitledWindowMask
backing:NSBackingStoreBuffered
defer:YES];
if (self) {
[self setLevel:NSFloatingWindowLevel];
NSRect frameRect = [self frameRectForContentRect:contentRect];
NSView* view = [[[FloatWindowContentView alloc]
initWithFrame:frameRect] autorelease];
[self setContentView:view];
[self setAcceptsMouseMovedEvents:YES];
[self setIgnoresMouseEvents:NO];
}
return self;
}
- (BOOL)becomesKeyOnlyIfNeeded
{
return YES;
}
- (void)becomeMainWindow
{
NSLog(#"becomeMainWindow");
[super becomeMainWindow];
}
- (void)becomeKeyWindow
{
NSLog(#"becomeKeyWindow");
[super becomeKeyWindow];
}
#end
#implementation FloatWindowContentView
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (id)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
if (self) {
NSTrackingArea* area;
area = [[NSTrackingArea alloc] initWithRect:frameRect
options:NSTrackingActiveAlways|
NSTrackingMouseMoved|
NSTrackingMouseEnteredAndExited
owner:self
userInfo:nil];
[self addTrackingArea:area];
[area release];
}
return self;
}
- (void)drawRect:(NSRect)rect
{
[[NSColor redColor] set];
NSRectFill([self bounds]);
}
- (BOOL)becomeFirstResponder
{
NSLog(#"becomeFirstResponder");
return [super becomeFirstResponder];
}
#end
I think I find the right answer. This is somehow related to tell NSWindow to accept MouseMoved event. but what to my surprise is, The window should accept mouseMoved events isn't the Floating Window, but the current Key window. So the solution is simple, just enable key window to accept mouse moved events before starting tracking, and revert the switch after ending tracking.

Wait for [NSAlert beginSheetModalForWindow:...];

When I display an NSAlert like this, I get the response straight away:
int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
response = [alert runModal];
The problem is that this is application-modal and my application is document based. I display the alert in the current document's window by using sheets, like this:
int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
[alert beginSheetModalForWindow:aWindow
modalDelegate:self
didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:)
contextInfo:&response];
//elsewhere
- (void) alertDidEnd:(NSAlert *) alert returnCode:(int) returnCode contextInfo:(int *) contextInfo
{
*contextInfo = returnCode;
}
The only issue with this is that beginSheetModalForWindow: returns straight away so I cannot reliably ask the user a question and wait for a response. This wouldn't be a big deal if I could split the task into two areas but I can't.
I have a loop that processes about 40 different objects (that are in a tree). If one object fails, I want the alert to show and ask the user whether to continue or abort (continue processing at the current branch), but since my application is document based, the Apple Human Interface Guidelines dictate to use sheets when the alert is specific to a document.
How can I display the alert sheet and wait for a response?
We created a category on NSAlert to run alerts synchronously, just like application-modal dialogs:
NSInteger result;
// Run the alert as a sheet on the main window
result = [alert runModalSheet];
// Run the alert as a sheet on some other window
result = [alert runModalSheetForWindow:window];
The code is available via GitHub, and the current version posted below for completeness.
Header file NSAlert+SynchronousSheet.h:
#import <Cocoa/Cocoa.h>
#interface NSAlert (SynchronousSheet)
-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow;
-(NSInteger) runModalSheet;
#end
Implementation file NSAlert+SynchronousSheet.m:
#import "NSAlert+SynchronousSheet.h"
// Private methods -- use prefixes to avoid collisions with Apple's methods
#interface NSAlert ()
-(IBAction) BE_stopSynchronousSheet:(id)sender; // hide sheet & stop modal
-(void) BE_beginSheetModalForWindow:(NSWindow *)aWindow;
#end
#implementation NSAlert (SynchronousSheet)
-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow {
// Set ourselves as the target for button clicks
for (NSButton *button in [self buttons]) {
[button setTarget:self];
[button setAction:#selector(BE_stopSynchronousSheet:)];
}
// Bring up the sheet and wait until stopSynchronousSheet is triggered by a button click
[self performSelectorOnMainThread:#selector(BE_beginSheetModalForWindow:) withObject:aWindow waitUntilDone:YES];
NSInteger modalCode = [NSApp runModalForWindow:[self window]];
// This is called only after stopSynchronousSheet is called (that is,
// one of the buttons is clicked)
[NSApp performSelectorOnMainThread:#selector(endSheet:) withObject:[self window] waitUntilDone:YES];
// Remove the sheet from the screen
[[self window] performSelectorOnMainThread:#selector(orderOut:) withObject:self waitUntilDone:YES];
return modalCode;
}
-(NSInteger) runModalSheet {
return [self runModalSheetForWindow:[NSApp mainWindow]];
}
#pragma mark Private methods
-(IBAction) BE_stopSynchronousSheet:(id)sender {
// See which of the buttons was clicked
NSUInteger clickedButtonIndex = [[self buttons] indexOfObject:sender];
// Be consistent with Apple's documentation (see NSAlert's addButtonWithTitle) so that
// the fourth button is numbered NSAlertThirdButtonReturn + 1, and so on
NSInteger modalCode = 0;
if (clickedButtonIndex == NSAlertFirstButtonReturn)
modalCode = NSAlertFirstButtonReturn;
else if (clickedButtonIndex == NSAlertSecondButtonReturn)
modalCode = NSAlertSecondButtonReturn;
else if (clickedButtonIndex == NSAlertThirdButtonReturn)
modalCode = NSAlertThirdButtonReturn;
else
modalCode = NSAlertThirdButtonReturn + (clickedButtonIndex - 2);
[NSApp stopModalWithCode:modalCode];
}
-(void) BE_beginSheetModalForWindow:(NSWindow *)aWindow {
[self beginSheetModalForWindow:aWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
}
#end
The solution is to call
[NSApp runModalForWindow:alert];
after beginSheetModalForWindow. Also, you need to implement a delegate that catches the "dialog has closed" action, and calls [NSApp stopModal] in response.
Here is a NSAlert category that solves the issue (as suggested by Philipp with the solution proposed by Frederick and improved by Laurent P.: I use a code block instead of a delegate, so it is simplified once again).
#implementation NSAlert (Cat)
-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow
{
[self beginSheetModalForWindow:aWindow completionHandler:^(NSModalResponse returnCode)
{ [NSApp stopModalWithCode:returnCode]; } ];
NSInteger modalCode = [NSApp runModalForWindow:[self window]];
return modalCode;
}
-(NSInteger) runModalSheet {
return [self runModalSheetForWindow:[NSApp mainWindow]];
}
#end
Just in case anyone comes looking for this (I did), I solved this with the following:
#interface AlertSync: NSObject {
NSInteger returnCode;
}
- (id) initWithAlert: (NSAlert*) alert asSheetForWindow: (NSWindow*) window;
- (NSInteger) run;
#end
#implementation AlertSync
- (id) initWithAlert: (NSAlert*) alert asSheetForWindow: (NSWindow*) window {
self = [super init];
[alert beginSheetModalForWindow: window
modalDelegate: self didEndSelector: #selector(alertDidEnd:returnCode:) contextInfo: NULL];
return self;
}
- (NSInteger) run {
[[NSApplication sharedApplication] run];
return returnCode;
}
- (void) alertDidEnd: (NSAlert*) alert returnCode: (NSInteger) aReturnCode {
returnCode = aReturnCode;
[[NSApplication sharedApplication] stopModal];
}
#end
Then running an NSAlert synchronously is as simple as:
AlertSync* sync = [[AlertSync alloc] initWithAlert: alert asSheetForWindow: window];
int returnCode = [sync run];
[sync release];
Note there is potential for re-entrancy issues as discussed, so be careful if doing this.
Unfortunately, there is not much you can do here. You basically have to make a decision: re-architect your application so that it can process the object in an asynchronous manner or use the non-approved, deprecated architecture of presenting application modal alerts.
Without knowing any information about your actual design and how you processes these objects, it's hard to give any further information. Off the top of my head, though, a couple of thoughts might be:
Process the objects in another thread that communicates with the main thread through some kind of run loop signal or queue. If the window's object tree gets interrupted, it signals the main thread that it was interrupted and waits on a signal from the main thread with information about what to do (continue this branch or abort). The main thread then presents the document-modal window and signals the process thread after the user chooses what to do.
This may be really over-complicated for what you need, however. In that case, my recommendation would be to just go with the deprecated usage, but it really depends on your user requirements.
Swift 5:
extension NSAlert {
/// Runs this alert as a sheet.
/// - Parameter sheetWindow: Parent window for the sheet.
func runSheetModal(for sheetWindow: NSWindow) -> NSApplication.ModalResponse {
beginSheetModal(for: sheetWindow, completionHandler: NSApp.stopModal(withCode:))
return NSApp.runModal(for: sheetWindow)
}
}
here is my answer:
Create a global class variable 'NSInteger alertReturnStatus'
- (void)alertDidEndSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
[[sheet window] orderOut:self];
// make the returnCode publicly available after closing the sheet
alertReturnStatus = returnCode;
}
- (BOOL)testSomething
{
if(2 != 3) {
// Init the return value
alertReturnStatus = -1;
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
[alert addButtonWithTitle:#"OK"];
[alert addButtonWithTitle:#"Cancel"];
[alert setMessageText:NSLocalizedString(#"Warning", #"warning")];
[alert setInformativeText:#"Press OK for OK"];
[alert setAlertStyle:NSWarningAlertStyle];
[alert setShowsHelp:NO];
[alert setShowsSuppressionButton:NO];
[alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:#selector(alertDidEndSheet:returnCode:contextInfo:) contextInfo:nil];
// wait for the sheet
NSModalSession session = [NSApp beginModalSessionForWindow:[alert window]];
for (;;) {
// alertReturnStatus will be set in alertDidEndSheet:returnCode:contextInfo:
if(alertReturnStatus != -1)
break;
// Execute code on DefaultRunLoop
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
// Break the run loop if sheet was closed
if ([NSApp runModalSession:session] != NSRunContinuesResponse
|| ![[alert window] isVisible])
break;
// Execute code on DefaultRunLoop
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
[NSApp endModalSession:session];
[NSApp endSheet:[alert window]];
// Check the returnCode by using the global variable alertReturnStatus
if(alertReturnStatus == NSAlertFirstButtonReturn) {
return YES;
}
return NO;
}
return YES;
}
Hope it'll be of some help,
Cheers
--Hans
This is the version of Laurent, et al., above, translated into Swift 1.2 for Xcode 6.4 (latest working version as of today) and tested in my app. Thanks to all those who contributed to make this work! The standard documentation from Apple gave me no clues as to how go about this, at least not anywhere that I could find.
One mystery remains to me: why I had to use the double exclamation point in the final function. NSApplication.mainWindow is supposed to be just an optional NSWindow (NSWindow?), right? But the compiler gave the error shown until I used the second '!'.
extension NSAlert {
func runModalSheetForWindow( aWindow: NSWindow ) -> Int {
self.beginSheetModalForWindow(aWindow) { returnCode in
NSApp.stopModalWithCode(returnCode)
}
let modalCode = NSApp.runModalForWindow(self.window as! NSWindow)
return modalCode
}
func runModalSheet() -> Int {
// Swift 1.2 gives the following error if only using one '!' below:
// Value of optional type 'NSWindow?' not unwrapped; did you mean to use '!' or '?'?
return runModalSheetForWindow(NSApp.mainWindow!!)
}
}
Unlike Windows I don't believe there's a way to block on modal dialogs. The input (e.g. the user clicking a button) will be processed on your main thread so there's no way of blocking.
For your task you will either have to pass the message up the stack and then continue where you left off.
When one object fails, stop processing the objects in the tree, make a note of which object failed (assuming that there is an order and you can pick up where you left off), and throw up the sheet. When the user dismisses the sheet, have the didEndSelector: method start processing again from the object that it left off with, or don't, depending on the returnCode.
- (bool) windowShouldClose: (id) sender
{// printf("windowShouldClose..........\n");
NSAlert *alert=[[NSAlert alloc ]init];
[alert setMessageText:#"save file before closing?"];
[alert setInformativeText:#"voorkom verlies van laatste wijzigingen"];
[alert addButtonWithTitle:#"save"];
[alert addButtonWithTitle:#"Quit"];
[alert addButtonWithTitle:#"cancel"];
[alert beginSheetModalForWindow: _window modalDelegate: self
didEndSelector: #selector(alertDidEnd: returnCode: contextInfo:)
contextInfo: nil];
return false;
}
You can use dispatch_group_wait(group, DISPATCH_TIME_FOREVER);:
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:#"alertMessage"];
[alert addButtonWithTitle:#"Cancel"];
[alert addButtonWithTitle:#"Ok"];
dispatch_async(dispatch_get_main_queue(), ^{
[alert beginSheetModalForWindow:progressController.window completionHandler:^(NSModalResponse returnCode) {
if (returnCode == NSAlertSecondButtonReturn) {
// do something when the user clicks Ok
} else {
// do something when the user clicks Cancel
}
dispatch_group_leave(group);
}];
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//you can continue your code here
Hope that helps.

Resources