detecting when NSUndoManager's alert view is visible - objective-c

I'm going to describe what I'm trying to do generally (in case there's a better way) and then the stumbling block I've run into (in case my way is the best way).
What I want to do: I want to add an invocation to my undo manager that has a time limit. If the undo is not triggered within the time limit, it won't be available when the device is shaken and thus nothing will happen.
What I'm doing: My approach was to use an NSUndoManager with an NSTimer. When I add the invocation to the undo manager, I kick off a 5-second timer as well. When the timer fires it checks !self.undoManager.isUndoing and if it's true than it goes ahead and removes all actions from the undo manager. Testing it in the simulator works: a shake gesture kicks off the undo before 5 seconds, but not after.
The problem is that if I get a shake gesture under the 5 second limit, the undo manager shows the alert, but if the user waits until after the 5s limit to actually tap the undo button, nothing happens: the timer happily cleared the stack away, even though the alert view was visible.
Is there a way to check and see if the alert view is visible? Best would be if I could figure out if the user hit undo or cancel as well, and clear the undo manager's action stack if the cancel button was pressed.
Or is there a better way besides using a timer in this manner?
Thanks!
Edited to add: My other thought was to capture the shake event myself (via the motionEnded:withEvent: call) and manually manage the alert and undo stack. Thoughts on this compared to the above are also welcome.

I ended up doing what I suggested in my edit—to use motionEnded:withEvent to manually manage the alert and undo. The downside to this is that you don't get the built-in undo alert which has a slightly different style from a UIAlertView and enters the screen with a shaking motion.
The upside is that I now have a undo that expires after 10 seconds. What follows is the general structure of the code in case you want the same.
First, make sure your app can receive shake events and that you have an NSUndoManager you can access. You also need a timer; I have my code set up with an NSTimer that kicks off when the undoable event occurs and lasts 10 seconds. Make sure you add your undo target at the same timer your timer starts so that there's actually something to undo.
Next, implement motionEnded:withEvent like so:
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake && [self.undoManager canUndo]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Undo something?" message:nil delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Undo", nil];
[alert show];
undoAlertIsVisible_= YES;
}
}
I'm using an ivar called undoAlertIsVisible_ here to track if my alert is on screen.
In your timer's callback, do something like:
if (!self.undoManager.isUndoing && !undoAlertIsVisible_) {
// Clear away the possible undo
[self.undoManager removeAllActionsWithTarget:self];
}
undoTimer_ = nil;
Here we check to see we're not currently undoing and the alert isn't visible. If so, remove the undo actions and set the timer (another ivar) to nil. I'm setting the timer to nil so that I can check whether it's fired in my alert callback, which is here:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex != alertView.cancelButtonIndex) {
if (self.undoManager.canUndo) {
[self.undoManager undo];
}
}
else {
if (!undoTimer_) {
// Timer fired while we were staring at the alert
[self.undoManager removeAllActionsWithTarget:self];
}
}
undoAlertIsVisible_= NO;
}
In the alert callback we either make the undo happen or, if the timer fired while the alert was visible and the alert was canceled, we clear possible undo actions. Otherwise the undo action would hang around after canceling with no timer to clear it.
Hope this helps someone!

Related

How to send app to background

On tvOS, I've only been able to get the begin states of button presses from the Siri remote by overriding the pressesBegan method on the view. If I use gesture recognizers, it only returns the end state. The catch is, when I override pressesBegan, even if I only use it for the select button, it still overrides the default function of the menu button (to push the app to the background). So I was looking into how to send the app to the background and call that method for the menu button (as is default behavior), but it appears that it is not kosher per Apple's standards to do that.
Here is my code for reference:
-(void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
for (UIPress* press in presses) {
switch (press.type) {
case UIPressTypeSelect:
NSLog(#"press began");
break;
case UIPressTypeMenu:
// this is where I would call the send to background call if Apple would allow that
// removing this case also has no effect on what happens
break;
default:
break;
}
}
As an alternative, this ONLY sends button release signals, but nothing presses begin.
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(gestureTap:)];
tapGesture.allowedPressTypes = #[[NSNumber numberWithInteger:UIPressTypeSelect]];
[view addGestureRecognizer:tapGesture];
When there's some behavior that happens if you don't override a method, and the behavior goes away in an empty override implementation, it stands to reason that behavior is provided by the superclass. (Cocoa is dynamic and complicated, so such inferences aren't true 100% of the time, but often enough.)
So, just call super for the cases where you don't want your override to change the default behavior:
case UIPressTypeMenu:
[super pressesBegan: presses withEvent: event];
break;

Is it possible to hide a button during a update process in Objective C?

I have the following problem:
I want that the user make a download when pressing a button. During this download, I want to hide the other buttons (which would open the downloaded files, so I want to ensure that no one tries to open files when the update haven't finished yet).
Is it possible to hide these buttons during this process?
So what I have tried and experienced so far:
Changes to the buttons I get always just at the end (when it isn't necessary anymore, because then the update is done).
I tried the following (Pseudocode):
-(void)updatingprogress
{
buttona.hidden=TRUE;
}
-(void)updatingfinished
{
buttona.hidden=FALSE;
}
updateFiles()
{
[self updatingprogress]
... make downloads...
[self updatingfinished]
}
So with logging I see, that I get in my functions at the moment I want, but the changes of the buttons aren't done during "updatingprogress". Any Idea how to solve this problem?
Thanks and best regards!
A common problem is that you are trying to update UI elements on a background thread. If your updateFiles method is happening on a different thread your button may not be hidden properly. To dispatch methods to the main threads you can either use the NSOperationQueue API or the GCD API.
NSOperationQueue:
[[NSOperationQueue mainQueue] addBlockOperation:^ {
buttona.hidden = YES;
}];
GCD:
dispatch_async(dispatch_get_main_queue(), ^ {
buttona.hidden = YES;
});
Both of these APIs do the same thing. I generally try to use the highest abstraction possible so in this case I would use the NSOperationQueue method
Another possibility is that you're doing all the work on the main thread, but failing to allow for the fact that, as a rule, UIKit changes don't take effect until you drop down to the runloop.
The background logic is that you don't want partial changes to be visible, so e.g. if you wrote:
// okay, set to success
label.textColor = [UIColor greenColor]; // was previously red
label.text = #"Success"; // previously said 'Failure'
What you explicitly don't want is for the word 'Failure' to appear in green, then for the word to change to 'Success'. You want the two changes to occur atomically. Apple achieve this by batching UIKit updates together and effecting them outside of any of your scheduled methods.
So if you have a function on the main thread that does some UI changes, does some work and then undoes the UI changes, but all without at any point exiting to the runloop, then the changes will never be seen.
The quickest solution would be:
- (void)updateFiles
{
[self updatingProgress];
[self performSelector:#selector(doFileUpdate) withObject:nil afterDelay:0.0];
// what does the above achieve? It schedules doFileUpdate on the runloop, to
// occur as soon as possible, but doesn't branch into it now. So when this
// method returns UIKit will update your display
}
- (void)doFileUpdate
{
/* heavy lifting here */
[self updatingFinished];
}

Block until NSAlert (shown as a modal sheet) is dismissed

I'm currently learning (by doing) objective-c by implementing a feature I felt is missing in the Titanium Appcelerator Desktop SDK: A way to do modal dialog with custom button texts and optionally to display them as a "sheet".
All is dandy and working, however, when displaying the NSAlert as a "sheet" my method that's creating the alert returns immediately and that's what I want to prevent.
The method's creating the alert returns an int (the return code from the NSAlert).
The code inside basically boils down to:
int returnCode = -1;
if (displayAsSheet) {
[alert beginSheetModalForWindow:nativeWindow modalDelegate:delegate didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
} else {
returnCode = [alert runModal];
}
return returnCode;
The modalDelegate is an object that's implementing the needed:
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
and for now it just does a NSLog of the returnCode.
So my question is:
How can I block my method from returning until the "sheet" has been dismissed?
Or am I going about this the wrong way?
You must start a modal session for you sheet after showing it and stop the session after closing sheet.
Check this: https://github.com/incbee/NSAlert-SynchronousSheet, I think it will be helpfull.
You could use this after beginSheetModalForWindow:...:
[[NSRunLoop mainRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate distantFuture]]
However, it will make any other windows in your app unusable until the sheet is dismissed. It would be better not to block those windows.
my method that's creating the alert returns immediately
I believe that's because, as #Josh says, the sheet is running modal only relative to the window to which it is attached; it is not freezing the entire app. Therefore, as soon as beginSheetModal... executes, the rest of your method continues to run, concluding with return returnCode (here returning -1), without waiting for the user to respond to the alert.
The return code is a stand-in for which button on the alert panel the user ends up pushing (NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, etc. -- they're listed at the end of the NSAlert class ref). You use it in your alertDidEnd method to act upon whichever button the user pushed to dismiss the alert. That's why the alertDidEnd selector includes the returnCode.
On the other hand, when you use the runModal method in your else block, you need to explicitly call alertDidEnd and feed it the number returned when the runModal method ends -- which is when the user dismisses the alert.
Here's a revised version of your code:
int returnCode = -1;
if (displayAsSheet) {
[alert beginSheetModalForWindow:nativeWindow modalDelegate:delegate didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
// The selector alertDidEnd has the returnCode int. The alert will then set that code to whatever the user chooses, and will send the altered int on to alertDidEnd.
}
else {
// Here, everything stops once runModal is called, until the user dismisses the alert, at which time the runModal method returns the int representing the button the user pushed, and you assign the return to your variable "returnCode."
returnCode = [alert runModal];
[self alertDidEnd:alert returnCode:returnCode contextInfo:nil];
}
// Omit the line returning the returnCode.
Then the alertDidEnd method does something like this:
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
switch (returnCode) {
case NSAlertFirstButtonReturn:
// Do whatever should happen when first button is pushed.
break;
case NSAlertSecondButtonReturn:
// Do whatever should happen when second button is pushed.
break;
default:
break;
}
// Unfreeze things.
[[NSApplication sharedApplication] stopModal];
}
By the way, there is a way of running a sheet and freezing the entire app, not just the window to which the sheet is attached, if that's what you want: modal tips
You're thinking about this slightly the wrong way. If your method were able to wait for the sheet to end, the app's event loop would be blocked and there would be no way for the user to interact with the UI. When you use runModal for the alert, a new run loop is created to handle the alert -- this is why nothing else can be done to the app. The point of the sheet option is to allow the user to do other things while the alert is displayed -- i.e., it expressly does not take over the event handling.
You could look into faking the sheet by attaching a child window.
You could try to set a boolean that freezes anything you want to freeze on your app (set freeze = YES) until the sheet is removed (set freeze = NO).
In the general case, you don't need to block a method : you just want some things not to happen until the user has made a choice.
For example, I have an app that uses gyroscope. It has some behaviour with, and some other behaviour without.
So I have a boolean that is used in any method that uses the gyro data to route the behaviour to the good one. My useGyro Boolean is NO when : the user is choosing what kind of hardware feature it want to enable or not, and when gyro is not available on the device.
The same thing with a mapView I have : when the user is aked by the system if it wants to be located, there is a moment where I freeze any behaviour using the user location. When he has made his choice, I change that bool value.

How does Apple update the Airport menu while it is open? (How to change NSMenu when it is already open)

I've got a statusbar item that pops open an NSMenu, and I have a delegate set and it's hooked up correctly (-(void)menuNeedsUpdate:(NSMenu *)menu works fine). That said, that method is setup to be called before the menu is displayed, I need to listen for that and trigger an asynchronous request, later updating the menu while it is open, and I can't figure out how that's supposed to be done.
Thanks :)
EDIT
Ok, I'm now here:
When you click on the menu item (in the status bar), a selector is called that runs an NSTask. I use the notification center to listen for when that task is finished, and write:
[[NSRunLoop currentRunLoop] performSelector:#selector(updateTheMenu:) target:self argument:statusBarMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
and have:
- (void)updateTheMenu:(NSMenu*)menu {
NSMenuItem *mitm = [[NSMenuItem alloc] init];
[mitm setEnabled:NO];
[mitm setTitle:#"Bananas"];
[mitm setIndentationLevel:2];
[menu insertItem:mitm atIndex:2];
[mitm release];
}
This method is definitely called because if I click out of the menu and immediately back onto it, I get an updated menu with this information in it. The problem is that it's not updating -while the menu is open-.
Menu mouse tracking is done in a special run loop mode (NSEventTrackingRunLoopMode). In order to modify the menu, you need to dispatch a message so that it will be processed in the event tracking mode. The easiest way to do this is to use this method of NSRunLoop:
[[NSRunLoop currentRunLoop] performSelector:#selector(updateTheMenu:) target:self argument:yourMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]]
You can also specify the mode as NSRunLoopCommonModes and the message will be sent during any of the common run loop modes, including NSEventTrackingRunLoopMode.
Your update method would then do something like this:
- (void)updateTheMenu:(NSMenu*)menu
{
[menu addItemWithTitle:#"Foobar" action:NULL keyEquivalent:#""];
[menu update];
}
(If you want to change the layout of the menu, similar to how the Airport menu shows more info when you option click it, then keep reading. If you want to do something entirely different, then this answer may not be as relevant as you'd like.)
The key is -[NSMenuItem setAlternate:]. For an example, let's say we're going to build an NSMenu that has a Do something... action in it. You'd code that up as something like:
NSMenu * m = [[NSMenu alloc] init];
NSMenuItem * doSomethingPrompt = [m addItemWithTitle:#"Do something..." action:#selector(doSomethingPrompt:) keyEquivalent:#"d"];
[doSomethingPrompt setTarget:self];
[doSomethingPrompt setKeyEquivalentModifierMask:NSShiftKeyMask];
NSMenuItem * doSomething = [m addItemWithTitle:#"Do something" action:#selector(doSomething:) keyEquivalent:#"d"];
[doSomething setTarget:self];
[doSomething setKeyEquivalentModifierMask:(NSShiftKeyMask | NSAlternateKeyMask)];
[doSomething setAlternate:YES];
//do something with m
Now, you'd think that that would create a menu with two items in it: "Do something..." and "Do something", and you'd be partly right. Because we set the second menu item to be an alternate, and because both menu items have the same key equivalent (but different modifier masks), then only the first one (ie, the one that is by default setAlternate:NO) will show. Then when you have the menu open, if you press the modifier mask that represents the second one (ie, the option key), then the menu item will transform in real time from the first menu item to the second.
This, for example, is how the Apple menu works. If you click once on it, you'll see a few options with ellipses after them, such as "Restart..." and "Shutdown...". The HIG specifies that if there's an ellipsis, it means that the system will prompt the user for confirmation before executing the action. However, if you press the option key (with the menu still open), you'll notice they change to "Restart" and "Shutdown". The ellipses go away, which means that if you select them while the option key is pressed down, they will execute immediately without prompting the user for confirmation.
The same general functionality holds true for the menus in status items. You can have the expanded information be "alternate" items to the regular info that only shows up with the option key is pressed. Once you understand the basic principle, it's actually quite easy to implement without a whole lot of trickery.
The problem here is that you need your callback to get triggered even in menu tracking mode.
For example, -[NSTask waitUntilExit] "polls the current run loop using NSDefaultRunLoopMode until the task completes". This means that it won't get run until after the menu closes. At that point, scheduling updateTheMenu to run on NSCommonRunLoopMode doesn't help—it can't go back in time, after all. I believe that NSNotificationCenter observers also only trigger in NSDefaultRunLoopMode.
If you can find some way to schedule a callback that gets run even in the menu tracking mode, you're set; you can just call updateTheMenu directly from that callback.
- (void)updateTheMenu {
static BOOL flip = NO;
NSMenu *filemenu = [[[NSApp mainMenu] itemAtIndex:1] submenu];
if (flip) {
[filemenu removeItemAtIndex:[filemenu numberOfItems] - 1];
} else {
[filemenu addItemWithTitle:#"Now you see me" action:nil keyEquivalent:#""];
}
flip = !flip;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
target:self
selector:#selector(updateTheMenu)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
Run this and hold down the File menu, and you'll see the extra menu item appears and disappears every half second. Obviously "every half second" isn't what you're looking for, and NSTimer doesn't understand "when my background task is finished". But there may be some equally simple mechanism that you can use.
If not, you can build it yourself out of one of the NSPort subclasses—e.g., create an NSMessagePort and have your NSTask write to that when it's done.
The only case you're really going to need to explicitly schedule updateTheMenu the way Rob Keniger described above is if you're trying to call it from outside of the run loop. For example, you could spawn a thread that fires off a child process and calls waitpid (which blocks until the process is done), then that thread would have to call performSelector:target:argument:order:modes: instead of calling updateTheMenu directly.

App modal NSPanel / sheet / dialog + NSThread == window hangs?

I'm in the midst of debugging an extremely unusual problem, and I was wondering if anybody might have any insight into what might be going wrong:
In a controller class from a NIB, I take an NSPanel from that same NIB, and then show it app modally on a NSWindow (that was created by hand in code):
[[NSApplication sharedApplication] beginSheet: myPanel
modalForWindow: window
modalDelegate: self
didEndSelector: #selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo: nil];
[[NSApplication sharedApplication] runModalForWindow: myPanel];
Now, when the "finish" button on that sheet is clicked, I run some code to disable some buttons and fire off a thread to make sure the user input is valid (I have to validate with a remote service). This thread is fired from a separate validator object I create:
// controller calls:
[validator validateCreds: creds
notify: #selector(validationComplete:)
onObject: self];
// validator object
validateInfo: (NSDictionary *)parms
notify: (SEL)notifySelector
onObject: (id)notifyObject
{
// build up data with parms and notify info
[[NSThread detachNewThreadSelector: #selector(remotevalidate:)
toTarget: self withObject: data];
}
Next, when the validation is finished, the validator notifies my controller object:
[notifyObject performSelectorOnMainThread: notifySelector
withObject: results waitUntilDone: NO];
And then my controller object, in the method that the validator object calls, kills the dialog:
- (void)validationComplete: (id)data
{
[[NSApplication sharedApplication] stopModal];
[createTwitterPanel orderOut: nil];
[[NSApplication sharedApplication] endSheet: createTwitterPanel
returnCode: NSOKButton];
}
- (void)sheetDidEnd:(NSWindow *)sheet
returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
m_returnCode = returnCode;
}
My problem: Although the panel is closed / disappears, the top NSApp runModalForWindow: does not exit until some system event is sent to the window that was showing the dialog. Trying to move, resize, or do anything to the window, or otherwise switching away from the application suddenly causes the method to exit and execution to continue. No amount of waiting seems to help, otherwise, however.
I have verified that all methods being invoked on the controller class are all being invoked on the main app thread.
An even more interesting clue is that the dialog has two controls, a WebView, and an NSTextField: Even if I force the exit of runModalForWindow: by clicking on the window, TABbing between the two controls remains screwed up — it simply never works again. It's like my event loop is horked.
I've tried changing validationComplete: to instead post a notification to the main thread, and I've also played with the waitUntilDone on the performSelectorOnMainThread method, all to no effect.
Any ideas? Things I should try looking at?
From the NSApplication documentation:
abortModal must be used instead of
stopModal or stopModalWithCode: when
you need to stop a modal event loop
from anywhere other than a callout
from that event loop. In other words,
if you want to stop the loop in
response to a user’s actions within
the modal window, use stopModal;
otherwise, use abortModal. For
example, use abortModal when running
in a different thread from the
Application Kit’s main thread or when
responding to an NSTimer that you have
added to the NSModalPanelRunLoopMode
mode of the default NSRunLoop.
So, I learned something today.