How can I make Cocoa bindings update during menu tracking? - objective-c

I'm using Cocoa bindings to control the enabling and disabling of buttons and menu items in my app, and in general it is working very nicely. However, there is one issue: the enabling does not update during menu tracking.
My app runs a long task, started by the user. That task runs in the background, and so control is returned to the run loop as soon as the task starts. The user can do other things while the task is going; however, there are some things that the user cannot do while a task is running, and some things that the user can do only when a task is running. I want menu items and buttons related to such things to disable/enable while the task is running, and enable/disable, reciprocally, when the task finishes.
In general this works great; I've got it all working. The only problem is that if the user is tracking through the menu bar at the point in time when the task finishes, the menu items do not change their enabling until the user stops tracking. This means that the user can actually select a menu item that ought to have been disabled, because the enable state of the menu item has not yet been updated.
So I just need a call that I can make at the moment my task finishes that triggers an immediate re-evaluation of enable states based on bindings. Is there a way?
(Note this is not related to multithreading. The task is actually performed on the main thread (for various reasons) using delayed perform requests. So the task finishes during such a run loop perform on the main thread, sets the "finished" property immediately, and then returns, allowing menu tracking to continue. The problem is that AppKit apparently does not do anything with enabling during the menu tracking.)
UPDATE:
As explained in the comments below, I am an idiot. I had switched over to using validateMenuItem: for my menu item enabling, and then forgot that I had done so. So the problem was real, but it was about validateMenuItem:, not about bindings. Anyway, for anybody who finds this post and actually wants to know how to force the menubar to re-update, the quick answer is -[NSMenu update], the long answer that I arrived at is:
- (void)forceImmediateMenuUpdate
{
// So, the situation is that the simulation has stopped playing because the end of the simulation was reached. If that happens while the user
// is tracking a menu in the menu bar, the menus do not get updating (enabling, titles) until the user ends menu tracking. This is sort of a
// bug in Cocoa, I guess; it assumes that state that influences menu bar items does not change while the user is tracking menus, which is false
// in our case, but is true for most applications, I suppose, maybe. Anyway, we need to force an immediate update in this situation.
NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
NSInteger numberOfSubmenus = [mainMenu numberOfItems];
for (int itemIndex = 0; itemIndex < numberOfSubmenus; ++itemIndex)
{
NSMenuItem *menuItem = [mainMenu itemAtIndex:itemIndex];
NSMenu *submenu = [menuItem submenu];
[submenu update];
}
}
I call that method at the point where my task finishes, and the menus all update themselves even as the user is tracking them. (Which is obviously a questionable thing to do in general, but here it is just enabling/disabling items, not shuffling them around under the user's mouse :->)
I hope this helps somebody. Sorry for the noise.

Related

OSX 10.10 App doesn't get focus or event loop

Restarting app development after a 15 year hiatus. Current project is conversion of old windows-type command line utility into interactive OS X windowed app.
I created a view delegate inside main window and can draw and update NSTable view.
The updates are generated in the App's main loop which takes a UDP/TCP stream, parses and updates view via appropriate delegation.
Here's the problem: When I run the app, the main window does not apparently get
focus (window control buttons on upper left are grey), the Menu created from my .xib is inert, and the window itself does not respond to resizing or to mouse hits inside the table view scroll bars. Also, the mouse pointer is the spinning beachball when over the App's window.
My guess is that I am not providing time to the Objective C run loop for event processing. I do send a "display" to my window on every iteration of my app loop, but I guess it is not sufficient (Apple is not very clear about what objects get what messages when sending this kind of update message). Am I on the right track?
Is there a way to let the system Event loop run an iteration each time through my app main loop?
Thanks!
Update: I tried explicitly providing event loop time in my app's main loop with:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
There was no apparent change in app behavior.
My guess is that I am not providing time to the Objective C run loop for event processing.
In this case, I recommend you read about Grand Central Dispatch, which provides concurrency, allowing the GUI to remain responsive.
There's a good explanation of GCD here and whilst it looks like a large and complicated subject, you'll probably only need a few lines of code to make use of GCD. For example: -
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// do some work
});
Despite comparing the nibs and connections between my code and a freshly created .xib prototype, i could find no reason for my code to not get focus.
The fix was to move my code guts into the project with a new .xib.
This works but is unsatisfying

How can I allow a user adjust an NSSlider without pausing the application update loop?

NOTE: Updated below...
I have a cocoa desktop application which consists of a series of controls around a custom NSView. I am using displayLink to drive the updates.
When a user clicks on an NSControl (a slider, a button, a checkbox, a radio button) the application appears to freeze until the mouse is released. I can confirm in fact that the displayLink callback (getFrameForTime) is NOT firing during the time. If I create a timer, that also does not fire, both remain paused until the user releases the mouse, at which point the application resumes updating.
The control is bound, and if I update that value from another thread (for example, via a callback from a MIDI interface) the slider behaves as expected: it moves, the value updates and the application does not pause.
I feel like this should be a fairly obvious fix, but I'm stumped.
Checking "continuous" in IB does as advertised: sends the values continuously, but still exhibits this behavior (preventing the UI update) until the mouse is released.
This seems to be related specifically to mouseDown on NSControl? Why would this block, and do I really need to subclass all my UI elements to change this behavior (seems extreme)
DisplayLink is in its own thread, so why mouseDown on the main thread block it? If this is the case, given the injunction on updating the Cocoa UI from other than the main thread, how do I deal with it?
Any help much appreciated.
Update
Per #Nikolai's comments below, I can confirm that using an NSTimer and adding it to NSEventTrackingRunLoopMode does NOT block. However, I would really like to use CVDisplayLink which (according to the documentation) runs in it's own thread and should not be blocked in this way. Unlike CADisplayLink, I cannot find a way to explicitly assign a runloop to CVDisplayLink (it seems it doesn't work that way), so perhaps the new question should be:
Why does CVDisplayLink block on NSEventTrackingRunLoopMode?
When clicking on an NSControl the runloop mode goes from NSDefaultRunLoopMode to NSEventTrackingRunLoopMode, as long as the mouse is down. That means that only run loop sources (display link) and timers fire that have been added to this mode.
You can add timers to any mode by using -[NSRunLoop addTimer:forMode:]. For a display link the equivalent method is -[CADisplayLink addToRunLoop:forMode:].
To make your animation continue during event tracking you would do something like:
[myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSEventTrackingRunLoopMode];
Your test project shows that you are calling a view's display method from within the display link's callback.
When commenting the display message out, the display link is called continuously even while moving the slider.
So what goes wrong is that when the runloop goes into event tracking mode, the call to display on the display link's thread blocks until the mouse is released and the run loop goes back to default mode. You can easily confirm this by putting a log statement before the call and one after it.
Why exactly that happens is not clear to me. What is clear is that it's illegal to call a view's methods from a background thread. You have to trigger the view's display by dispatching a setNeedsDisplay: on the main thread:
static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
{
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge MyCustomView*)displayLinkContext setNeedsDisplay:YES];
});
return kCVReturnSuccess;
}

Best practice for a long-running foreground operation that uses Core Data?

I have an app that imports a potentially large amount of data from the web after the user explicitly presses a Sync button, and stores that data using Core Data. Since I want to show feedback and I don't want the user interacting with the rest of the app while this happens, pressing the Sync button brings up a Modal dialog. Since I want the operation to happen immediately, the operation executes in the viewDidAppear method. I'm sure this is frowned upon.
There are a bunch of problems with the approach right now:
Everything happens in the main thread. The user kind of gets feedback because there is an activity indicator that continues to animate, but there's no way to indicate progress or show intermediate messages. This is not the right way to do things.
But, I am told that when using Core Data, everything has to use the main thread, so breaking off the work into another thread does not seem like it will be straightforward.
If the app enters the background state (user hits Home button or iPad falls sleep), it's game over - the operation dies. It's clear to me from the documentation why this is the case.
I know there are "I'm about to enter the background" events that you can handle, but it's not as though I can move execution of code from one place to another in the middle of a file download. Whatever solution I use has to be a continuous action that executes in the same way both before and after the transitions to/from the background.
I want the operation to execute in the foreground as far as the user is concerned. It does not make sense for them to interact with other parts of the app while this operation is taking place.
I am reading the Apple documentation on this, but I'm asking this in hopes of finding more concise guidance on this particular combination of needs. Thanks.
You really should not freeze the main thread. You can still "prohibit" certain UI actions.
Create a separate context, as a child, and do all your work in there. When done (or at certain intervals), save the context to the main context, and notify the main thread to do some UI update interaction... maybe a progress bar or something...
NSManagedContext *backgroundContext = [NSManagedContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroudContext.parentContext = [self mainManagedObjectContext];
[backgroundContext performBlock:^{
// This block is running in a background thread.
// Go get your data from the web
// Call this to push data to the main MOC (either at end, or at intervals)
[backgroundContext save:&error];
// When you want to do something on the main thread...
dispatch_async(dispatch_get_main_queue(), ^{
// This block is running on the main queue... I can do anything with the UI...
}];
}];
Couple of things to note... your mainMOC needs to be private or main queue concurrency type. If you are using the Core Data template, where it is in the app delegate, just change the alloc/init to initWithConcurrencyType:NSMainQueueConcurrencyType.
I would, however, suggest using the canonical main/parent relationship. Create a private MOC, assign it to the persistent store, then create a main MOC, set its parent to be that private MOC. Now you are ready to handle any I/O with background operations, without blocking your UI.
Still, when loading from the web, use the pattern above: create a child MOC, then load objects into the main MOC.
Note, that the data is not saved to disk until the "root" MOC calls save.

Alternatives to applicationDidEnterBackground and applicationWillResignActive?

I've got an app that changes the screen brightness with [UIScreen mainScreen].brightness = newBrightness, and I want to restore the brightness to it's previous state when the user finishes using it.
I've tried these two delegate methods:
- (void)applicationDidEnterBackground:(UIApplication *)application
- (void)applicationWillResignActive:(UIApplication *)application
But without much success. I suspect my app must be in the foreground to change the brightness? When I change the brightness in didEnterBackgroundMethod, it has no effect at all. When I use willResignActive it does restore the brightness if I switch to another app, but it has no effect when I press the home button.
Are there any notifications or delegate methods that are executed before the app leaves the foreground?
It seems this happens to others as well: see this S.O. post.
Only way around it seems to be forgetting about setBrightness and simulating it by overlaying a black-semi-transparent on your view...
OLD ANSWER:
willResignActive should also be called when you press the home button before the application enters the background state.
This method is called to let your application know that it is about to move from the active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. An application in the inactive state continues to run but does not dispatch incoming events to responders.
This is also the behavior I witness. So, my guess (but it's just a guess) is that your app is not set to support background, so that when pressing the home button it is terminated. In this case applicationDidEnterBackground is not called.
I would suggest to check the info.plist file in your project for the UIApplicationExitsOnSuspend or "Select Application does not run in background" key.
Furthermore, you could try and put some breakpoints (or NSLog traces) in those functions and check whether they are effectively called as expected.
According to Apple´s DevForum it seems to be a bug that Apple don´t want to fix soon.

Event path in Mozilla plugin on Mac

I'm writing a mozilla plugin on the mac. I'm trying to capture events like button clicks, etc. I've done the normal thing of creating my views in inteface builder, and linking the sentActions to methods in my program. This works in stand-alone programs.
However, in my NPAPI plugin, those methods never get called. The button reacts, depresses, whatever, but it doesn't do its action.
Instead, the NPP_HandleEvent method gets called, but I never get the MouseDown or MouseUp event, only the UpdateEvt.
I set up the buttons to accept clicks via: (superview is the Mozilla view, topview is the top of my view hierarchy.)
[superView setNextResponder: topView];
[topView setNextResponder: nil];
[browserWindow makeFirstResponder: topView];
NEVER MIND: I'm an idiot. It IS calling the button sent actions. I was looking at the wrong method. That'll teach me to leave around a zoom: method when I'm actually using a doZoom: method... D'oh,.
So, the problem was that I wasn't able to get buttons to work. The buttons were supposed to (for example) zoom an image in an IKImageView. (or rather, zoom the view). It didn't appear that it was working. The screen was flashing a lot, but nothing was happening... I put a printf in my zoom method, and it was NEVER GETTING CALLED! and so I asked the question.
Later, I noticed that I wasn't TRYING to call zoom, I was calling doZoom! doZoom WAS being called. And the reason that it wasn't zooming was an unrelated problem.
The problem ended up being that I was sending setImage to my IKImageView on every event, which re-set the view to 1-1, rightside up mode. Once I took out the extra setImage call, things started to work.
In the unlikely event that anyone else every experiences this, the answer is my cunning plan for world domination:
step 1: Don't be an idiot.
step 2: ???????
step 3: Dominate the world.
(If I could master step 1, I might just be able to figure out what step 2 was B-)