I'm using the Dropbox API for OSX. All works fine, except when I want to make calls in a modal window that is started with [NSApp runModalForWindow:thisWindow]; it seems the modal loop blocks the
DropboxAPI from processing anything.
The DBRestClient delegate methods are never called in response to for example [client loadMetadata:path]; Which is - if understand correctly - in line with what the NSApplication documentation says for this method. The question is:
Is there a way to let calls to Dropbox work from inside a modal window?
I have seen that timers can be added to the NSModalPanelRunLoopMode. Is there perhaps something similar for the DroboxAPI?
And additionally: will Dropbox calls that were started but not yet completed before this or any other modal window is displayed proceed as normal, or are they also blocked?
Yes; further investigation shows any runModalForWindow and even displaying an NSAlert.showModal will completely block the DropboxAPI. Also, inplace mouse handling loops do the same thing. Imo a major design flaw in the OSX DropboxAPI: it should have been running on a background thread. The only way around this, is to not start any user task that could involve blocking Dropbox while the API something is still running. Which is not really feasible in any non-trivial app that needs dropbox to work in the background.
Implementation of NSApplication runModalForWindow is indeed too harsh.
Here is more humanistic version:
void runWindowModal(NSWindow* pnw) {
[pnw retain];
NSApplication* app = [NSApplication sharedApplication];
NSModalSession session = [app beginModalSessionForWindow:pnw];
for( ;; ) {
if ([app runModalSession:session] != NSModalResponseContinue)
break;
NSEvent* e = [app nextEventMatchingMask: NSAnyEventMask
untilDate: [NSDate distantFuture]
inMode: NSDefaultRunLoopMode
dequeue: YES];
if (e != nil)
[app sendEvent: e];
}
[app endModalSession: session];
[pnw release];
}
This will enable NSURLConnection and other callbacks processing while running modal loops.
Dropbox support just confirmed that the DropboxAPI on OSX works on the main runloop and indeed runModalForWindow etc. will block the API. There's no work around.
The problem is more general. All nsurl connections can run only on the main runloop. This means that webviews have the same issue. And there is a sollution to this. You have to create a modal session and run it for short amounts of time, frequently, and lock the main thread in a loop until the modal window is closed. Every time you switch back to the modal session, the main runloop will finish any scheduled tasks, and that includes the nsurlconnections from the modal session.
Now I will show you a piece of code but note that this is c# using Xamarin.Mac library. You should be able to translate this easily to objc. But if you find it difficult, you can look up solutions for nswebview in modal dialog.
//so this is in my NSWindowController - the modal dialog
IntPtr session;
bool running;
public void showDialog ()//this is the method I use to show the dialog
{
running = true;
session = NSApplication.SharedApplication.BeginModalSession (Window);
int resp = (int)NSRunResponse.Continues;
while (running && resp == (int)NSRunResponse.Continues) {
resp = (int)NSApplication.SharedApplication.RunModalSession (session);
NSRunLoop.Current.RunUntil (NSRunLoopMode.Default, NSDate.DistantFuture);
}
NSApplication.SharedApplication.EndModalSession (session);
}
[Export ("windowWillClose:")]
public void WindowWillClose (NSNotification notification)//this is the override of windowWillClose:
{
running = false;
}
Related
My code results in the sheet appearing and functioning properly but the invoker receives control back (as per Apple definition) before the sheet ends itself with endSheet.
How can I get the invoker to wait for the return from the end of the sheet processing, so the resultValue is updated.
Invoker:
[self.window beginSheet: sheetController.window
completionHandler:^(NSModalResponse returnCode) {
resultValue = returnCode;
}
];
...
Sheet:
...
[self.window.sheetParent endSheet:self.window returnCode:false];
I thought the new beginSheet: was meant to do the whole linkage, but that isn't the case. All it does is put up the sheet window. The passage and return of control remains as it was. So the following code works:
Invoker:
[self.window beginSheet: sheetController.window
completionHandler:nil
];
returnCodeValue = [NSApp runModalForWindow:sheetController.window];
// Sheet is active until stopModalWithCode issued.
[NSApp endSheet: sheetController.window];
[sheetController.window orderOut:self];
Sheet:
[NSApp stopModalWithCode:whatever];
Looks like you're most of the way there. The return code should be type NSModalResponse (ObjC) or NSApplication.ModalResponse (Swift). I've also found that you need to uncheck 'Visible at Launch'. Otherwise it won't launch as modal.
There was an NSAlert category from way back that used the older (deprecated) methods. I’m not all that great with Objective-C (and worse with Swift), but I’ve been using an updated equivalent for a while in one of my RubyMotion projects. Hopefully I’ve come close with reversing the code conversions, but basically it fires up a modal event loop with runModalForWindow: after calling beginSheetModalForWindow;, and the completionHandler exits with stopModalWithCode:
// NSAlert+SynchronousSheet.h
#import <Cocoa/Cocoa.h>
/* A category to allow NSAlerts to be run synchronously as sheets. */
#interface NSAlert (SynchronousSheet)
/* Runs the receiver modally as a sheet attached to the specified window.
Returns a value positionally identifying the button clicked */
-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow;
/* Same as above, but runs the receiver modally as a sheet attached to the
main window. */
-(NSInteger) runModalSheet;
#end
// NSAlert+SynchronousSheet.m
#import "NSAlert+SynchronousSheet.h"
#implementation NSAlert (SynchronousSheet)
-(NSInteger) runModalSheetForWindow:(NSWindow *)theWindow {
// Bring up the sheet and wait until it completes.
[self beginSheetModalForWindow:theWindow
completionHandler:^(NSModalResponse returnCode) {
// Get the button pressed - see NSAlert's Button Return Values.
[NSApp stopModalWithCode:returnCode];
}];
[NSApp runModalForWindow:self.window] // fire up the event loop
}
-(NSInteger) runModalSheet {
return [self runModalSheetForWindow:[NSApp mainWindow]];
}
#end
The application will wait for the alert to finish before continuing, with the modal response passed in the result. I don’t know about blocking a couple of thousand lines of code, but I found it useful when chaining two or three sheets, such as an alert before an open/save panel.
In my document-based MacOS application, I have some big files that are loaded (especially recent files opened at application launch). I created ProgressController (a NSWindowController subclass) to inform user in a window that file loading is in progress. It is allocated by the makeWindowControllers method of the NSDocument subclass I use to manage documents. These are created when the user opens a file (and notably at startup when documents were displayed as the user did quit the application) and they load their content asynchronously in a background queue which in principle should not affect the main thread performance:
-(instancetype) initForURL:(NSURL *)urlOrNil withContentsOfURL:(NSURL *)contentsURL ofType:(NSString *)typeName error:(NSError *
{
if (self = [super init]){
... assign some variables before loading content...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{
[[[NSURLSession sharedSession] dataTaskWithURL:urlOrNil completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
self.content = [[NSMutableString alloc] initWithData:data encoding:encoder];
dispatch_async(dispatch_get_main_queue(), ^(){
[self contentIsLoaded];
});
}] resume];
});
}
return self;
}
In contentIsLoaded, the progress window is closed.
- (void) contentIsLoaded
{
... do something ...
[self.progressController close];
}
This behaviour is OK, the window is displayed and closed when necessary.
The problem occurs when I want to update the content of this window on the main queue. I tried setting a NSTimer but is is never fired, even though it is created in the main queue. So, in MyApplicationDelegate I created a GCD timer like this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if (self->timer !=nil) dispatch_source_cancel(timer);
self.queue = dispatch_queue_create( "my session queue", DISPATCH_QUEUE_CONCURRENT);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
[self updateProgress];
});
dispatch_resume(timer);
…
}
In MyApplicationDelegate, the updateProgress method is defined as:
- (void) updateProgress
{
dispatch_async(self.queue, ^{
NSLog(#"timer method fired");
dispatch_async(dispatch_get_main_queue(), ^(){
NSLog(#"access to UI fired");
... update window (e.g. NSProgressIndicator)
}
});
if (self.shouldCancelTimer) dispatch_source_cancel(self->timer);
});
}
When running the application, The "Timer method fired" is logged every second. The "timer method fired" message (in the main queue) is logged once or twice only, then the logging seems suspended until the file has been loaded. Then this missing message appears several times in a row, as it was suspended before.
What did I wrong? I supposed that the background queue used for file loading should not affect the main queue and UI updates. Many applications behave like that and I need such behaviour as files in my App (strings, csv, json) can be hundreds of Mbytes!
For starters, have you looked into NSURLSessionTask's progress reporting APIs? There are a bunch of facilities for measuring and getting called back as data loads. It might be possible for you to avoid doing the polling, and just update your UI as these are called back - all on the main thread.
In fact, you might not even need one at all. As I recall, the networking operations carried out by NSURLSession are all done in the background anyways. I bet you can remove all of this, and just use some extra delegate callbacks from NSURLSession APIs to get this done. Way simpler too :)
Good luck!
A bit of context first. Essentially I have a window that is covering the desktop. On it I have a few WebKit WebView views which allow user interaction. By deafult, as one would expect, when another application is active it does not receive these events (such as hovering, mouse entered, and clicking). I can make it work by clicking my window first, then moving the mouse, but this is not good for usability. I've also managed to make it activate the window when the cursor enters, but it's far from ideal and rather hacky.
So instead I'm trying to use a tracking area. At the moment on the WebViews superview I have this tracking area:
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self visibleRect]
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveAlways
owner:self
userInfo:nil];
This works as I want it to, I'm receiving the all the mouse events. However, the WebViews don't seem to be responding as intended. JavaScript mouse move events only fire when I hold and drag, not just hover and drag.
I've tried using hitTest to get the correct view, but nothing seems to work. Here's an example method, I'm using the isHandlingMouse boolean because without it an infinite loop seemed to be created for some reason:
- (NSView *)handleTrackedMouseEvent: (NSEvent *)theEvent{
if(isHandlingMouse)
return nil;
isHandlingMouse = true;
NSView *hit = [self hitTest: theEvent.locationInWindow];
if (hit && hit != self) {
return hit;
}
return nil;
}
- (void)mouseMoved:(NSEvent *)theEvent{
NSView *hit = [self handleTrackedMouseEvent: theEvent];
if (hit){
[hit mouseMoved: theEvent];
}
isHandlingMouse = false;
}
The 'hit' view, is a WebHTMLView, which appears to be a private class. Everything seems like it should be working,but perhaps there's something I'm doing that's breaking it, or I'm sending the event to the WebHTMLView incorrectly.
Post a sample Xcode project to make it easier for people to test solutions to this problem.
I was doing something similar and it took a lot of trial and error to find a solution. You will likely need to subclass NSWindow and add - (BOOL)canBecomeKeyWindow { return YES; }, then whenever you detect the mouse is over your window, you might call [window orderFrontRegardless] just so it can properly capture the mouse events.
Brad Larson delivered a solution for the CADisplayLink freeze issue when scroll views are scrolling.
My OpenGL ES draw method is called by a CADisplayLink, and I tried Brad's technique but can't make it work. The core problem is that my OpenGL ES view is hosted by a UIScrollView, and when the UIScrollView scrolls, the CADisplayLink stops firing.
The technique Brad described is supposed to let the CADisplayLink continue to fire even during scrolling (by adding it to NSRunLoopCommonModes instead of the default runloop mode), and using a fancy semaphore trick the rendering callback is supposed to ensure that it doesn't render when UIKit is too occupied.
The problem is though that the semaphore trick prevents the rendering callback from drawing, no matter what.
First, I create the serial GCD queue and semaphore in the initWithFrame method of my OpenGL ES view like this (on the main thread):
frameRenderingQueue = dispatch_queue_create("com.mycompany.crw", DISPATCH_QUEUE_SERIAL);
frameRenderingSemaphore = dispatch_semaphore_create(1);
The display link is created and added to NSRunLoopCommonModes:
CADisplayLink *dl = [[UIScreen mainScreen] displayLinkWithTarget:self selector:#selector(renderFrame)];
[dl addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
The render callback performs Brad's technique:
- (void)renderFrame {
if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0) {
NSLog(#"return"); // Gets called ALWAYS!
return;
}
dispatch_async(drawingQueue, ^{
#autoreleasepool {
// OpenGL ES drawing code
dispatch_semaphore_signal(frameRenderingSemaphore);
}
});
}
The dispatch_semaphore_wait function always returns YES and thus the render callback never renders. Even when I'm not scrolling.
I suppose that I missed something important here. Can someone point it out?
Edit: It seems to work only when I call dispatch_sync instead of dispatch_async, but according to Brad dispatch_async would give better performance here.
I had to change the structure of the code to this:
- (void)renderFrame {
dispatch_async(drawingQueue, ^{
if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0) {
return;
}
#autoreleasepool {
// Drawing code...
}
dispatch_semaphore_signal(frameRenderingSemaphore);
});
}
After I restructured it this way, the dispatch_semaphore_wait call stopped returning YES all the time. I am not sure if this is effectively just disabling Brad's semaphore wait trick or not. But it works.
I've tried calling
modalSession=[NSApp beginModalSessionForWindow:conversionWindow];
[NSApp runModalForWindow:conversionWindow];
in order to get a modal conversionWindow that prevents the user to interact with the rest of the application, but this also seems to block code execution. What I mean is that the code that comes after the code shown above isn't executed at all. How can I fix this? I'm sure this is possible because many applications show some progress while performing some big task, like video conversion etc...
Please don't use an app-modal window unless it's absolutely necessary. Use a sheet if possible. However, if you must use a modal dialog, you can make the main run loop run by giving it some time while the modal dialog is open:
NSModalSession session = [NSApp beginModalSessionForWindow:[self window]];
int result = NSRunContinuesResponse;
while (result == NSRunContinuesResponse)
{
//run the modal session
//once the modal window finishes, it will return a different result and break out of the loop
result = [NSApp runModalSession:session];
//this gives the main run loop some time so your other code processes
[[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode];
//do some other non-intensive task if necessary
}
[NSApp endModalSession:session];
This is very useful if you have views that require the main run loop to operate (WebView comes to mind).
However, understand that a modal session is just that, and any code after the call to beginModalSessionForWindow: will not be executed until the modal window closes and the modal session ends. This is one very good reason not to use modal dialogs.
Note that you must not do any significant work in the while loop in the code above, because then you will block your modal session as well as the main run loop, which will turn your app into beachball city.
If you want to do something substantial in the background you must use some form of concurrency, such as a using NSOperation, a GCD background queue or just a plain background thread.
You need to start the background "task" in another thread if you want it to run while a modal window is up. But in most cases, the best solution is to not use a modal window.
That is how a modal window works: the window is presented synchronously, and the code after NSApp.runModal(for: window) is executed after the modal window session is stopped.
Under the hood, the modal window runs in a special modal event loop, so that only the events happen in the specified window are processed.
In code, it does stop the execution at the point of NSApp.runModal(for: window), but you can still dispatch jobs from the window. For example, a button on the modal window that triggers a download task.
You can use:
performSelector(onMainThread:with:waitUntilDone:modes:) to dispatch a job.
GCD to dispatch a job.
One common scenario that you feel the modal window blocks code execution can be:
// present a modal window in GCD main queue asynchronously:
DispatchQueue.main.async {
NSApp.runModal(for: window)
// only executes after modal window is dismissed
}
and somewhere else tries to dispatch using GCD. For example:
// modal window's button action handler:
button.actionHandler = {
DispatchQueue.main.async {
// some button action...
}
}
Because NSApp.runModal(for: window) doesn't leave the dispatched block, the GCD main queue won't execute following blocks until the modal window is dismissed.
To avoid this blocking issue, you could:
Avoid calling NSApp.runModal(for: window) in DispatchQueue.main.
Use performSelector(onMainThread:with:waitUntilDone:modes:) to dispatch a job.
For the 2nd solution, I made a convenient helper for myself:
public func onMainThread(waitUntilDone: Bool = false, block: #escaping BlockVoid) {
_ = MainRunloopDispatcher(waitUntilDone: waitUntilDone, block: block)
}
private final class MainRunloopDispatcher: NSObject {
private let block: BlockVoid
init(waitUntilDone: Bool = false, block: #escaping BlockVoid) {
self.block = block
super.init()
performSelector(onMainThread: #selector(execute), with: nil, waitUntilDone: waitUntilDone)
}
#objc private func execute() {
block()
}
}
So that when the GCD main queue is blocked, you can use to call some code on the main thread
onMainThread {
// here is on main thread, update UI...
}
To read more, this article helped me.