How to make NSMenu drop down periodically without user input - objective-c

I have created a statusbar menu item on the top of my window like this:
- (void)createStatusBarItem {
_statusBar = [NSStatusBar systemStatusBar];
_statusItem = [_statusBar statusItemWithLength:NSSquareStatusItemLength];
_statusItem.image = [NSImage imageNamed:#"icon_off.png" ];
_statusItem.highlightMode = YES;
_statusItem.menu = [self createStatusBarMenu];
}
with the menu:
- (NSMenu *)createStatusBarMenu {
NSMenu *menu = [[NSMenu alloc] init];
...
}
I am now wanting to periodically make the menu drop down as if a user had clicked on the status bar icon. Is that even possible?

It is indeed. First create a repeating NSTimer to call a method repeatedly and store it as a property (so you can invalidate it later, etc.)
self.timer = [NSTimer timerWithTimeInterval: 5.0 target: self selector: #selector(displayMenu:) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer: self.timer forMode: NSRunLoopCommonModes];
Then in the method it calls, display the menu.
[self.statusItem popUpStatusItemMenu: self.statusItem.menu];
In this case it'll pop up every 5 seconds or so. However since you don't know when it closes, the user could close it and then have the timer call and reopen it a second later. So you might want to set yourself as the menu's delegate and implement menuWillOpen: and menuDidClose:. In menuDidClose: you'd probably remake the timer (or set it's fire date) in order to reset the interval so that it'll wait another 5 seconds before opening the menu from the time the menu last closed. You could also create a timer to close the menu automatically if the user didn't close it by themselves in menuWillOpen:, etcetera, so that the menu opens every X seconds and closes after X seconds by itself if nothing is done with it (you'd call the NSMenu's cancelTracking for that.)

Related

NSMenuItem custom view not updating

I have a NSStatusItem object which is created when the app launches (in AppDelegate.m). This object "_statusBarItem" has a Menu attached to it, of class statusBarMenu, which is subclass of NSMenu class but it also has a _panelItem property (of class NSMenuItem) created when you create an instance of statusBarMenu, as you can see below.
In statusBarItem.m
- (instancetype)initWithTitle:(NSString *)aTitle{
self = [super initWithTitle:aTitle];
if (self){
_panelItem = [[NSMenuItem alloc]init];
PanelViewController *panelViewController = [[PanelViewController alloc]init];
panelViewController.menuDelegate = self;
_panelItem.view = panelViewController.view;
[self addItem:_panelItem];
}
return self;
}
The _panelItem has a custom view i.e. a clock in a label (among other things). This view is controlled by PanelViewController class, in which when viewDidLoad method is called calls the tickTock: method shown below. _upTime is the label showing, the clock/time. It is created in the .xib file and connected
- (void)tickTock:(id)obj{
NSTimeInterval timeInterval = 7.5 * 60 * 60;
NSDate *timeToGetUp = [NSDate dateWithTimeInterval:timeInterval sinceDate:[NSDate date]];
NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:#"hh:mm:ss"];
[_upTime setStringValue:[dateFormatter stringFromDate:timeToGetUp]];
[self.view setNeedsDisplay:YES];
[self.menuDelegate refreshView:self];
[self performSelector:#selector(tickTock:) withObject:NULL afterDelay:1.0];
}
As you can see that tickTock: method is called every 1.0 second. This is because I want the label to update, every second with new time. However, the label does not update even though I call setNeedsDisplay: for the PanelViewController's view. I thought this might be because I might be updating the wrong view i.e. I should have been updating the _panelItem's view, instead. So I made a menuDelegate property and made statusBarMenu conform to the protocol show below.
#protocol PanelViewControllerMenuDelgate
- (void)refreshView:(id)obj;
#end
Again the refreshView: method is called every second, it updates the panel view.
- (void)refreshView:(id)obj{
[_panelItem.view setNeedsDisplay:YES];
// [self itemChanged:_panelItem];
}
However, that still does not refresh the view, and show the new label value. I also tried itemChanged: method to the statusBarMenu obj (_statusBarItem) itself, although it did not have any different results.
What I did notice is that if I close the menu (by clicking somewhere else) and re-open it, it does show the new value of the clock/label. So what am I doing wrong, or what am I not doing, that is making the view stay the same. Should I be using some other procedure to make the _panelItem's view refresh every second?
EDIT:
I also noticed that the simulation stop running, when every click the _statusBarItem. I simply added a NSLog statement in tickTock: method and it stopped printing in the console, when ever I open the menu. This is probably why the view is not updating, since the app/Xcode pauses when ever I click the menu. Why does that happen? Also how can I stop it?
I just came up against this problem a few weeks ago. In my case, my NSTimer was not updating my NSMenuItem. I believe your problem is happening because the menu is updating on a different run loop to the perform with delay. This question is answered in a few places. But this is the code you need:
NSTimer interfaceUpdateTimer = [NSTimer timerWithTimeInterval:self.timerSettings.timerUpdateInterval//one second in your case
target:self
selector:#selector(tickTock:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:interfaceUpdateTimer forMode:NSRunLoopCommonModes];
The key is NSRunLoopCommonModes. Check out the other answers I linked to; they already explain it really well. That should do the trick I think.
As a side note, neither the NSTimer nor the performSelector:withObject:afterDelay: are guaranteed to fire at exactly the time you specify. So if you need accuracy, do not rely on them to tell the time. If close enough is good enough, then you can ignore this note : )

Flipping a UISwitch with an NSTimer

I'm trying to learn how to use NSTimers, and I thought of the following: Create a switch. Let the timer begin as the app begins, and after each second, a function that changes the state of the switch is called.
Here's what I did so far:
I declared both the timer and the switch in the header file ViewControl.h:
//Timer
{NSTimer *timer;}
#property (weak, nonatomic) IBOutlet UISwitch *zeSwitch;
Then, in the ViewControl.m file I defined the following:
- (IBAction)zeSwitch:(id)sender {
UISwitch *zeSwitchSatus = (UISwitch *) sender;
BOOL yn = zeSwitchSatus.isOn;
[zeSwitch setOn:yn animated:YES];
}
- (void)viewDidLoad
{
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self
selector:#selector(zeSwitch) userInfo:nil repeats:YES];
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
So my hope was that when I run the code, I'll see a switch that is on or off. Then I'll see it changing its status automatically with time, without me interfering.
But that didn't work! I first get the image above. Nothing changes. Then it crashes when I press the switch. (But my idea is not to touch it at all.)
Any ideas?
You're pretty close. There's a few things wrong here. First, the method that you're giving to the timer is named zeSwitch: -- the colon is significant. So you need to create the timer like this:
timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(zeSwitch:)
userInfo:nil //^ Note colon!
repeats:YES];
The method named zeSwitch without the colon is actually the accessor method for the switch, because you've named your property zeSwitch. You should really rename the timer's action method to clarify this. Right now, the timer is calling the accessor method for the switch every second, which doesn't really do anything.
Next, the timer passes itself to the method it calls. The sender argument in zeSwitch: is going to be the timer, not the switch. If this method was actually being called via the timer, you would get a crash because you'd be sending isOn to the timer, and it doesn't respond to that.
You've got an outlet to the switch, so you can refer to it via that outlet:
- (void)flipSwitch: (NSTimer *)tim
{
BOOL switchIsOn = [[self zeSwitch] isOn];
Notice that I've corrected the names and types in this method -- you'll also need to change the timer creation to reflect this: #selector(flipSwitch:).
Third, you want to flip the switch, so you should be setting it to the opposite of its current status. The next line needs to be:
[[self zeSwitch] setOn:!switchIsOn animated:YES];
The ! operator negates the BOOL to which it's attached, turning YES into NO and vice versa.
1) When you specify a selector that takes one parameter, you need a colon after the name, so #selector(zeSwitch:).
2) The selector that is triggered by a timer gets the timer as a parameter, not a switch, so - (IBAction)zeSwitch:(NSTimer *)timer.

Opening a new window and waiting for it to close

I have a Mac OS X app written in objetive-c Cocoa. You can see most of the code in this previous question. Essentially you click a button on the main window (the app delegate) and it opens another window where the user can enter information.
In the following code (that gets called when the user press the button in the app's main window)
- (IBAction)OnLaunch:(id)sender {
MyClass *controllerWindow = [[MyClass alloc] initWithWindowNibName:#"pop"];
[controllerWindow showWindow:self];
NSLog(#"this is a log line");
}
The NSLog line gets printer immediately after I called showWindow. Is there any way to wait until controllerWindow is closed to continue with the NSlog?
The reason for this is that the user set's a value on the new window I opened and I need to collect that value on the same OnLaunch so I need to wait.
I know that modal windows are bad form in Mac, but I have no control over this feature.
I've tried with
[NSApp runModalForWindow:[controllerWindow window]];
and then setting the popup window to
[[NSApplication sharedApplication] runModalForWindow:popupwin];
and it works but then the focus never gets passed to the main window anymore
Thanks!
If you want the window to be modal for your application, use a sheet: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Sheets/Tasks/UsingCustomSheets.html
However, there is no way to suspend execution of a method while the sheet is displayed, this would be tantamount to blocking the current run loop. You would have to break you code into the begin and end methods as described in the linked documentation.
Here are the steps you need to follow:
In TestAppAppDelegate create an NSWindow outlet to hold your sheet and an action to dismiss the sheet
Create a nib with an NSWindow as the root object. I think you already have this in "pop". Set the Visible at Launch option to NO (this is very important)
Set the file's owner of this nib to TestAppAppDelegate and connect the window to your new outlet, and the close button to your new action
In your method to launch the sheet (OnLaunch), use the following code:
(ignore this it's to make the code format properly!)
if(!self.sheet)
[NSBundle loadNibNamed:#"Sheet" owner:self];
[NSApp beginSheet:self.sheet
modalForWindow:self.window
modalDelegate:self
didEndSelector:#selector(didEndSheet:returnCode:contextInfo:)
contextInfo:nil];
Your close button action should be [NSApp endSheet:self.sheet];
Your didEndSheet: method should be [self.sheet orderOut:self];
You can use UIVIew method animateWithDuration:delay:options:animations:completion: to accomplish this.
You said you want the next line to execute once the window is closed, rather than after it is opened. In any case, you may end the OnLaunch method this way:
- (IBAction)OnLaunch:(id)sender {
MyClass *controllerWindow = [[MyClass alloc] initWithWindowNibName:#"pop"];
[controllerWindow animateWithDuration:someDelay:options: someUIAnimationOption
animations:^{
[controllerWindow showWindow:self]; // now you can animate it in the showWindow method
}
completion:^{
[self windowDidFinishShowing]; // or [self windowDidFinishDisappearing]
}
}
- (void) windowDidFinishShowing {
NSLog(#"this is a log line");
}

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.

NSTimer with a menu bar app

I'm working on a simple timer app, and I've created a NSStatusItem with a menu and I have some NSTextField labels that updates the timer labels (http://cld.ly/e81dqm) but when I click on the status item the NSTimer stops (and stops updating the labels)..... how can I get around this problem?
EDIT: here's the code that starts the timer:
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(timerDidUpdate:) userInfo:nil repeats:YES];
You should add timer into MainRunLoop as given below:
NSRunLoop * rl = [NSRunLoop mainRunLoop];
[rl addTimer:timer forMode:NSRunLoopCommonModes];
I'm guessing the timer resumes as soon as you stop interacting with the NSStatusItem? (After the menu's dismissed & mouse button released).
The user interaction puts the main run loop into a mode where it doesn't update timers, so if your label has to continually update, you'll probably need to move the NSTimer and the label drawing to a separate process or another thread.