NSPopover transiency when popover is in status bar - cocoa

I'm making an app which lives in status bar. When status item is clicked, NSPopover pops up.
It looks like this:
Here's the problem: I want it to be "transient", that is if I click anywhere outside of the popover, it will close. And while NSPopoverBehaviorTransient works fine when popover is in a window, it doesn't work when it's in status bar.
How can I implement such behavior myself?

It turned out to be easy:
- (IBAction)openPopover:(id)sender
{
// (open popover)
if(popoverTransiencyMonitor == nil)
{
popoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask|NSRightMouseDownMask handler:^(NSEvent* event)
{
[self closePopover:sender];
}];
}
}
- (IBAction)closePopover:(id)sender
{
if(popoverTransiencyMonitor)
{
[NSEvent removeMonitor:popoverTransiencyMonitor];
popoverTransiencyMonitor = nil;
}
// (close popover)
}
What wasn't easy, though, is that there are nasty issues with having a popover pop out of NSStatusItem (it didn't behave as desired when Mission Control was invoked or space switched to a full-screen window). I had to implement a custom window that always floats above the NSStatusItem and deals with switching to a full-screen window etc. It seemed easy, but clearly status items weren't designed for something like that ;)

The approach that I use is similar to the above answer except I have everything combined into one method instead of using two separate IBActions.
First, I declare the following properties
#property (strong, nonatomic) NSStatusItem *statusItem;
#property (strong, nonatomic) NSEvent *popoverTransiencyMonitor;
#property (weak, nonatomic) IBOutlet NSPopover *popover;
#property (weak, nonatomic) IBOutlet NSView *popoverView;
then in awakeFromNib I set up the status bar item
- (void)awakeFromNib {
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
self.statusItem.title = #"Title";
self.statusItem.highlightMode = YES;
self.statusItem.action = #selector(itemClicked:);
}
followed by the method that is called when the status bar item is clicked
- (void)itemClicked:(id)sender {
[[self popover] showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinYEdge];
if (self.popoverTransiencyMonitor == nil) {
self.popoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:(NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyUpMask) handler:^(NSEvent* event) {
[NSEvent removeMonitor:self.popoverTransiencyMonitor];
self.popoverTransiencyMonitor = nil;
[self.popover close];
}];
}
}
which makes the popover appear and also close when the user clicks outside the view.
Note that in Interface Builder you must set the behavior of the popover to Transient so the popover will close when the user clicks the status item.

Related

Get a reference to status menu item and change its title

I have a Mac OS app with a status menu (made in Interface Builder). I want to change status menu item title when certain event happens. I can do it just fine inside the action handler, because I have a reference to the item there (sender):
- (IBAction)playPauseMusic:(id)sender {
// ...
[sender setTitle:#"New Title"];
}
But how to do it in other parts of my app? I don't know how to get a reference to menuItem in the following code:
- (void) someOtherMethod:(int)isPlaying {
menuItem = ...;
if(isPlaying) {
[menuItem setTitle:#"Pause"];
}
}
What to do to make the above work?
Update. Here's how I attach the status menu:
// MyAppDelegate.h:
#interface MyApp : NSApplication
#end
#interface MyAppDelegate : NSObject <NSApplicationDelegate>
{
NSMenu *statusMenu;
NSStatusItem *statusItem;
// ...
}
#property (strong) IBOutlet NSMenu *statusMenu;
// ...
#end
// MyAppDelegate.m:
#synthesize statusMenu;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
statusItem = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSVariableStatusItemLength];
[statusItem setMenu:[self statusMenu]];
}
If you want to access an item within the menu, then set a tag on the menu item (in IB, for example) and use:
NSMenuItem *menuItem = [[statusItem menu] itemWithTag:100]; // 100 = example
menuItem.title = #"Something";
If the code that wants to set the title is not the same object that holds statusItem then you'll need to expose a setStatusMenuTitle:forItemWithTag: method which performs the above code.
I cannot tell you how to get access to that object without more details, however.
First, I created an outlet for the menu item in Xcode. I followed the "Create and connect a new outlet" video guide. I made a strong outlet for my menu item:
#property (strong) IBOutlet NSMenuItem *playMenuItem;
Then added it to the interface:
#interface MyAppDelegate ...
{
// ...
NSMenuItem *playMenuItem;
}
Then added #synthesize declaration in implementation:
#synthesize playMenuItem;
Finally, the following worked:
[playMenuItem setTitle:#"New Title"];
P.S. If anyone can show me how to make it simpler, I'll be grateful.

Open NSWindowController from NSMenu

I'm with a NSMenu in an agent application (without the icon in the dock). When a button from this menu is tapped, I want to show a generic NSWindowController.
My menu button action:
- (IBAction)menuButtonTapped:(id)sender {
MyWindowController *myWindow = [[MyWindowController alloc] initWithWindowNibName:#"MyWindowController"];
[myWindow showWindow:nil];
[[myWindow window] makeMainWindow];
}
But the window just "flashes" in the screen (it shows and disappears really fast).
Any solution?
The reason the window is showing up for a split second and then disappearing has to do with ARC and how you go about creating the instance of the window controller:
- (IBAction)menuButtonTapped:(id)sender {
MyWindowController *myWindow = [[MyWindowController alloc]
initWithWindowNibName:#"MyWindowController"];
[myWindow showWindow:nil];
[[myWindow window] makeMainWindow];
}
Under ARC, the myWindow instance will be valid for the scope where it is defined. In other words, after the last [[myWindow window] makeMainWindow]; line is reached and run, the window controller will be released and deallocated, and as a result, its window will be removed from the screen.
Generally speaking, for items or objects you create that you want to "stick around", you should define them as an instance variable with a strong property.
For example, your .h would look something like this:
#class MyWindowController;
#interface MDAppController : NSObject
#property (nonatomic, strong) MyWindowController *windowController;
#end
And the revised menuButtonTapped: method would look something like this:
- (IBAction)menuButtonTapped:(id)sender {
if (self.windowController == nil) {
self.windowController = [[MyWindowController alloc]
initWithWindowNibName:#"MyWindowController"];
}
[self.windowController showWindow:nil];
}
Use this:
[[myWindow window] makeKeyAndOrderFront:self];

Custom Sheet : can't click buttons

I used this source http://www.cats.rwth-aachen.de/library/programming/cocoa
to create my custom sheet.
I created a NSPanel object in existing .xib file and connected with IBOutlet
My source code:
.h
#interface MainDreamer : NSWindow <NSWindowDelegate>
{
...
NSPanel *newPanel;
}
...
#property (assign) IBOutlet NSPanel *newPanel;
.m
#dynamic newPanel;
...
//this method is wired with button on main window and calls a sheet
- (IBAction)callPanel:(id)sender
{
[NSApp beginSheet:newPanel
modalForWindow:[self window] modalDelegate:self
didEndSelector:#selector(myPanelDidEnd:returnCode:contextInfo:)
contextInfo: nil]; //(__bridge void *)[NSNumber numberWithFloat: 0]
}
//this method is wired with cancel and ok buttons on the panel
- (IBAction)endWorkPanel:(id)sender
{
[newPanel orderOut:self];
[NSApp endSheet:newPanel returnCode:([sender tag] == 9) ? NSOKButton : NSCancelButton];
}
//closing a sheet
- (void)myPanelDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == NSCancelButton) return;
else{
return;
}
}
So callPanel works fine, sheet appears but I can't interact with controls on the sheet (with buttons). They don't react on a click (even visually).
Where the problem lies?
Heh, I forgot about
[newDreamPanel close];
in applicationDidFinishLaunching method. I wrote it because I wanted the panel to not appear when main window launches.
In fact, the Visible At Launch panel's property should be activated in IB. The close method works too, but side effect is that all controls become unable on the panel.

How to deal with a Toggle NSButton?

My application contains a PLAY/PAUSE button that is set to type Toggle in Interface Builder. I use it - as the name reveals - to play back my assets or to pause them.
Further, I am listening to the SPACE key to enable the same functionality via the keyboard shortcut. Therefore, I use keyDown: from NSResponderin my application. This is done in another subview. The button itself is not visible at this time.
I store the current state of playback in a Singleton.
How would you update the title/alternative title for the toogle button while taking into account that its state could have been altered by the keyboard shortcut? Can I use bindings?
I managed to implement the continuous update of the button title as follows. I added a programmatic binding for the state (in the example buttonTitle). Notice, that the IBAction toggleButtonTitle: does not directly change the button title! Instead the updateButtonTitle method is responsible for this task. Since self.setButtonTitle is called the aforementioned binding gets updated immediately.
The following example shows what I tried to describe.
// BindThisAppDelegate.h
#import <Cocoa/Cocoa.h>
#interface BindThisAppDelegate : NSObject<NSApplicationDelegate> {
NSWindow* m_window;
NSButton* m_button;
NSString* m_buttonTitle;
NSUInteger m_hitCount;
}
#property (readwrite, assign) IBOutlet NSWindow* window;
#property (readwrite, assign) IBOutlet NSButton* button;
#property (readwrite, assign) NSString* buttonTitle;
- (IBAction)toggleButtonTitle:(id)sender;
#end
And the implementation file:
// BindThisAppDelegate.m
#import "BindThisAppDelegate.h"
#interface BindThisAppDelegate()
- (void)updateButtonTitle;
#end
#implementation BindThisAppDelegate
- (id)init {
self = [super init];
if (self) {
m_hitCount = 0;
[self updateButtonTitle];
}
return self;
}
#synthesize window = m_window;
#synthesize button = m_button;
#synthesize buttonTitle = m_buttonTitle;
- (void)applicationDidFinishLaunching:(NSNotification*)notification {
[self.button bind:#"title" toObject:self withKeyPath:#"buttonTitle" options:nil];
}
- (IBAction)toggleButtonTitle:(id)sender {
m_hitCount++;
[self updateButtonTitle];
}
- (void)updateButtonTitle {
self.buttonTitle = (m_hitCount % 2 == 0) ? #"Even" : #"Uneven";
}
#end
If you store your state in an enum or integer a custom NSValueTransformer will help you to translate a state into its button title equivalent. You can add the NSValueTransformer to the binding options.
NSDictionary* options = [NSDictionary dictionaryWithObject:[[CustomValueTransformer alloc] init] forKey:NSValueTransformerBindingOption];
[self.button bind:#"title" toObject:self withKeyPath:#"buttonTitle" options:options];

COCOA: NSStatusitem icon disappearing on launching external code

I am a .Net developer that needs to port a small program over to Mac OS X. I have this mostly done (partly thanks to people on this site, thanks!) but have a bug that maybe people I can get help with.
I am creating a tool that sits in the status bar, that when clicked opens a window with several links or buttons. When the links or buttons are clicked they either open a website or external program. The problem is that the icon in the status bar disappears as I launch one of these external commands. Even more interesting is that the space on the status bar where the icon should be still responds; meaning that if I click on the area (even without the visible icon) it still runs the code and opens the window.
Here is the current code:
tray.m
#import "tray.h"
#import "MyView.h"
#implementation Tray
-(void) awakeFromNib{
NSBundle *bundle = [NSBundle mainBundle];
statusItem = [[NSImage alloc] initWithContentsofFile:[bundle pathForResource:"#icon" ofType:#"png"]];
MyView *view = [MyView new];
[statusItem setImage:statusImage];
view.image = statusImage;
[statusitem setView:view];
[statusitem setToolTip:#"Tray App"];
[view setTarget:self];
[view setAction:#selector(openWindow)];
}
-(IBAction)openWindow:(id)sender{
[trayWin makeKeyAndOrderFront:nil];
}
-(IBAction)openActMon:(id)sender {
(void)system("open '\/Applications/Utilities/Activity Monitor.app'");
}
tray.h
#import "MyView.h"
#interface Tray : NSObject {
NSStatusItem *statusItem;
NSImage *statusImage;
IBOutlet NSWindow * trayWin;
IBOutlet NSButton *ActMon;
void *openWindow;
}
#property (retain,nonatomic) NSStatusItem *statusItem;
-(IBAction)ActMon:(id)sender;
#end
MyView.h
#interface MyView : NSControl {
NSImage *image;
id target;
SEL action;
}
#property (retain)NSImage *image;
#property (assign) id target;
#property (assign) SEL action;
#end
MyView.m
#import "MyView.h"
#implementation MyView;
#synthethize image, target, action;
-(void)mousemouseUP:(NSEvent *)event{
[NSApp sendAction:selfself.action to:self.target from:self];
}
-(void)dealloc {
self.image = nil;
[super dealloc];
}
-(void)drawRect:(NSRect)rect {
[self.image drawInRect:CGRectMake(0,0,18,18) fromRect:NSZeroRect operation:NSCompositeSourceOver];
}
#end
}
The openActMon is run when the image/button is clicked, the image is located in the trayWin Window that is opened when the icon is clicked. At this point, Activity monitor successfully launches, but the icon in the StatusBar disappears.
I have tried putting a [super setNeedsDisplay:YES] in the openActMon, but that didn't help. And I added [view setNeedsDisplay:YES] in the openActMon and it responded undeclared.
I have given all of this code because, as I said, I am not a Objective-C coder, but .Net who just needs to port something small over. Hoping that this will be helpful to others in the future. Alot of this I have hodgepodged together from different forums and sites or have gotten from some help on StackOverflow. I am hoping someone can help.
Thanks in advance!
In awakeFromNib add:
[statusitem retain];
In awakeFromNib, you are allocating the NSImage into statusItem. I think you mean to allocate it into statusImage.

Resources