Popover NSStatusItem - cocoa

I'm playing around with an idea and basically I want a NSStatusItem with a NSPopoverController. I read about all the problem people had but I just want to try it. Is there a clean way to do it by now? All the versions I've seen are at least 1 year old and suuuuper hacky.
This was my approach so far but if I click my app in the statusbar nothing happens...
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
//[self.statusItem setView:view];
[self.statusItem setTitle:#"Test"];
[self.statusItem setHighlightMode:YES];
[self.statusItem setAction:#selector(activatePopover:)];
}
-(IBAction)activatePopover:(id)sender
{
BOOL isEnabled = NO;
if (isEnabled) {
[self.popover showRelativeToRect:NSMakeRect(0, 0, 50, 50) ofView:statusItem.view preferredEdge:NSMinYEdge];
} else {
[self.popover close];
}
}
Any ideas how to get this running?
Thanks

This will not work without using a custom view on the status item. If you don't set a custom view, the view property will be empty (it only returns custom views, not whatever view NSStatusItem uses internally when you just use setTitle).
Unfortunately, as per Apple's docs, you'll need to provide your own view and handle clicks yourself if you want to use NSPopover.
I haven't seen a complete example that encompasses correct handling of this (the default implementation of status items does rather a lot which you will have to do all manually), and also fixes popover wonkynesses:
NSPopover, by default, won't become the key window (some controls won't work), unless you overwrite canBecomeKeyWindow of NSPopover's window
Correctly dismissing menus of other status items (you can call popUpStatusItemMenu with an empty menu to correctly focus your status item)
Drawing the highlighted background with drawStatusBarBackgroundInRect
Reacting to both left and right mouse clicks
Using NSRunningApplication.currentApplication.activateWithOptions to make sure all windows of your status item become active (otherwise your popover will, erratically, not be the receiver of keyboard input)
Dismissing the NSPopover with NSEvent.addGlobalMonitorForEventsMatchingMask (the built-in dismissal mechanism popovers come with doesn't work with status items)
Removing the status item on termination with NSStatusBar.systemStatusBar.removeStatusItem
I hope to have a blog post about this out sometime soon (note: I'm using RubyMotion, not Objective-C), that explains all these issues and hopefully provides an easier base to create menulets. I'll update this comment if I write that post.

Code:
-(void)initializeStatusBarItem
{
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
NSImage* image = [NSImage imageNamed:#"image"];
// [image setTemplate:YES];
self.statusItem.button.image = image;
self.statusItem.highlightMode = NO;
self.statusItem.button.action = #selector(statusBarItemDidClick:);
}
- (void)statusBarItemDidClick:(NSStatusBarButton *)sender{
MainViewController *mainView = [[MainViewController alloc] init];
self.popoverView = [[NSPopover alloc] init];
[self.popoverView setContentViewController:mainView];
self.popoverView.contentSize = CGSizeMake(300, 400);
self.popoverView.behavior = NSPopoverBehaviorTransient;
[self.popoverView showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSMaxYEdge];
}

Related

UITableView becomes unresponsive

UITableViewCell becomes unresponsive this was a very different problem with a very different solution.
My tableView which is a subView in a UIViewController initially works fine and I can select individual rows in the table. However, I have created my own popup when a row is selected (the popup is a UIView) that appears towards the bottom of the screen. As this pops-up I also create a another UIView which covers the screen behind the popup and it makes the background go dim. The third thing that happens is that i create a UITapGestureRecogniser to keep track of the user's taps, and if they tap outside the UIView then the two UIViews and the TapGestureRecogniser are removed and call the deselectRowAtIndex... method.
However, it is at this point that I cannot use the tableView, as i want to be able to select a different string within the tableView and the popup to appear again (the popup will eventually contain links that will enable the user to move to different viewControllers).
I have tried to reload the data, remove the tableview and replace it, edit the didSelectRowAtIndex, remove the deselectRowAtIndex method, however nothing I tried seems to work and i can't find anything on stackoverflow as my question seems to be quite specific (although I apologise if there is something out there).
I'll add a few parts of my code in, however, I'm not sure where the problem is and I may not have copied the right part in.
The remove overhead is the selector method from the tapGesture
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(_popOverView == nil)
{
_popOverView = [[UIView alloc]initWithFrame:CGRectMake(20, 200, 280, 150)];
_popOverView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"wood.jpeg"]];
}
if(_mask == nil)
{
_mask = [[UIView alloc] initWithFrame:self.view.frame];
[_mask setBackgroundColor:[UIColor colorWithWhite:0.0 alpha:0.78]];
}
if (_tapDetector == nil)
{
_tapDetector= [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(removeOverHead:)];
}
[self.view addSubview:_mask];
[self.view addSubview:_popOverView];
[self.view addGestureRecognizer:_tapDetector];
}
-(void) removeOverHead:(UITapGestureRecognizer*) sender
{
CGPoint locationOfTap = [_tapDetector locationInView:self.view];
if (locationOfTap.y < (200 + 150) && locationOfTap.y > 200 && locationOfTap.x > 20 && locationOfTap.x < (20 + 280) ) {
NSLog(#"%f,%f",[_tapDetector locationInView:self.view].x,[_tapDetector locationInView:self.view].y);
}
else
{
[_mask removeFromSuperview];
[_popOverView removeFromSuperview];
[_tapDetector removeTarget:self action:#selector(removeOverHead:)];
/*this idea doesn't work :(
[self.tableView removeFromSuperview];
[self.view addSubview:_tableView];*/
}
}
I really hope the answer is in here and is very simple, and thank you in advance for taking the time to read this.
Solved it! Sorry for wasting your time. It was the wrong remove method for the gestureRecogniser. I replaced
[_tapDetector removeTarget:self action:#selector(removeOverHead:)]
with
[self.view removeGestureRecognizer:_tapDetector]
as the UIGestureRecogniser was lingering and obstructing the tableView!!
If you stick a breakpoint or NSLog() inside the else block of that remove method, do you get inside it?
It sounds like your if statement might be off. You should use CGRectContainsPoint(). However if I understand correctly, you're attempting to dismiss everything when the user taps the dimming background view. You could make this view a button or you could compare the touch's view pointer to the pointer to the background view.

PDFViewAnnotationHitNotification not being delivered

I'm implementing a PDF viewer on the Mac and I want to let the user add annotations.
I've added a PDFAnnotationText to the page, and it appears just fine, but when the user clicks on it, the whole document is shrunk and an annotation list appears down the left side.
I want to customize this to display the annotation as a pop-up, similar to what Preview does. The PDFAnnotationText class reference says I can do this:
Each PDFAnnotationText object has a PDFAnnotationPopup object associated with it. In its closed state, the annotation appears as an icon. In its open state, it displays as a pop-up window containing the text of the note. Note that your application must do the work to put up a window containing the text in response to a PDFViewAnnotationHitNotification.
But when I add an observer for PDFViewAnnotationHitNotification, no notification is delivered when I click on the annotation.
I've contacted Apple about this, and the answer I received back was that it's a bug. A workaround is to handle the mouse click yourself, walk the annotations and look for a hit.
Something like this (code which runs in a mouseDown handler in a PDFView subclass):
NSPoint windowPoint = [self.window convertScreenToBase:[NSEvent mouseLocation]];
NSPoint viewPoint = [self convertPoint:windowPoint fromView:nil];
PDFPage *page = [self pageForPoint:viewPoint nearest:NO];
if (page != nil) {
NSPoint pointOnPage = [self convertPoint:viewPoint toPage:page];
for (PDFAnnotation *annotation in page.annotations) {
NSRect annotationBounds;
// Hit test annotation.
annotationBounds = [annotation bounds];
if (NSPointInRect(pointOnPage, annotationBounds))
{
NSLog(#"Annotation hit: %#", annotation);
}
}
}

How to add the Navigation Bar's view to a PopOver's PassThroughViews?

I have a PopoverController view that allows a user to download a file. On button press, the popOver view will expand in size, display download status, and the main view controller will be obscured by an unhidden "cover" view that has been added to the PopoverController's "passThroughViews" property so that the user can not accidentally dismiss the pop over while the file is downloading.
My problem is that, in storyboards, my main viewController is embedded in a Navigation Controller. I can't seem to cover the navigation controller's bar with a view in the storyboard, and if the user presses anywhere on the navigation bar then the popover will disappear and the user will lose the download's progress bar.
How do I either cover up the navigation bar with my "cover" view, or how do I add the navigation bar's view to my popOverController's passThroughViews?
Opening the Popover from the main viewController:
- (IBAction)openDataOptionsPopOver:(id)sender
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
PopOverViewController *optionsWindow = [storyboard instantiateViewControllerWithIdentifier:#"dataOptions"];
self.popUp = [[UIPopoverController alloc] initWithContentViewController:optionsWindow];
[self.popUp setDelegate:self];
[nextNavButton setEnabled:NO]; //Disabling barButtonItem on the navigationController
optionsWindow.containerPopOver = self.popUp; //Pointer to the popover, to resize it later.
optionsWindow.coverView = self.coverView; //Pointer to the coverView, to (un)hide later
[popUp presentPopoverFromRect:[sender frame] inView:[sender superview] permittedArrowDirections:UIPopoverArrowDirectionDown animated:YES];
}
Setting the passThroughViews property inside of the PopoverViewController:
//Expands the popOver on press of "refreshFileButton" to display progressView
-(void) explodeWindow
{
//setting self.navigationController.view and ...visibleViewController.view here didn't seem to work ...
[containerPopOver setPassthroughViews:[NSArray arrayWithObjects:coverView, nil]];
[containerPopOver setPopoverContentSize:CGSizeMake(600, 400) animated:YES];
[titleBarItem setTitle:#"Downloading File. Please Wait ..."];
[refreshFileButton setHidden:YES];
[progressView setHidden:NO];
[downloadLabel setHidden:NO];
[coverView setHidden:NO];
[progressView setProgress:0.0 animated:NO];
}
I've tried adding self.navigationController.view to passThroughViews with no success--it actually turns out to be a null pointer. And I can't seem to place a UIView at any level in storyboards that will cover all my controls without obscuring the popOver. What am I missing here? And thanks for reading.
Edit:
As Aglaia points out below out, implementing the following, and avoiding passThroughViews, is probably the best way to do this.
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
//Don't dismiss our popover when the view covering our controls is present
if([coverView isHidden]){
return YES;
}else{
return NO;
}
}
Maybe there is something I am missing, but why don′t you just implement a new view controller with its navigation bar set to none and present it modally on button press? Then when the download is finished you just dismiss the view controller.
If you want the user to see the underlying view you can use a UIAlertView instead.
Alternatively set you view controller as the delegate of the popover controller and forbid the user to dismiss your popover on touch outside through
- (BOOL) popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
return NO;
}
Then when you want to dismiss it call dismissPopoverAnimated:
to cover the whole screen including navigation bar:
[myView setFrame:[[UIScreen mainScreen] bounds];
[self.navigationController.view addSubview:myView];

Custom NSView in NSMenuItem not receiving mouse events

I have an NSMenu popping out of an NSStatusItem using popUpStatusItemMenu. These NSMenuItems show a bunch of different links, and each one is connected with setAction: to the openLink: method of a target. This arrangement has been working fine for a long time. The user chooses a link from the menu and the openLink: method then deals with it.
Unfortunately, I recently decided to experiment with using NSMenuItem's setView: method to provide a nicer/slicker interface. Basically, I just stopped setting the title, created the NSMenuItem, and then used setView: to display a custom view. This works perfectly, the menu items look great and my custom view is displayed.
However, when the user chooses a menu item and releases the mouse, the action no longer works (i.e., openLink: isn't called). If I just simply comment out the setView: call, then the actions work again (of course, the menu items are blank, but the action is executed properly). My first question, then, is why setting a view breaks the NSMenuItem's action.
No problem, I thought, I'll fix it by detecting the mouseUp event in my custom view and calling my action method from there. I added this method to my custom view:
- (void)mouseUp:(NSEvent *)theEvent {
NSLog(#"in mouseUp");
}
No dice! This method is never called.
I can set tracking rects and receive mouseEntered: events, though. I put a few tests in my mouseEntered routine, as follows:
if ([[self window] ignoresMouseEvents]) { NSLog(#"ignoring mouse events"); }
else { NSLog(#"not ignoring mouse events"); }
if ([[self window] canBecomeKeyWindow]) { dNSLog((#"canBecomeKeyWindow")); }
else { NSLog(#"not canBecomeKeyWindow"); }
if ([[self window] isKeyWindow]) { dNSLog((#"isKeyWindow")); }
else { NSLog(#"not isKeyWindow"); }
And got the following responses:
not ignoring mouse events
canBecomeKeyWindow
not isKeyWindow
Is this the problem? "not isKeyWindow"? Presumably this isn't good because Apple's docs say "If the user clicks a view that isn’t in the key window, by default the window is brought forward and made key, but the mouse event is not dispatched." But there must be a way do detect these events. HOW?
Adding:
[[self window] makeKeyWindow];
has no effect, despite the fact that canBecomeKeyWindow is YES.
Add this method to your custom NSView and it will work fine with mouse events
- (void)mouseUp:(NSEvent*) event {
NSMenuItem* mitem = [self enclosingMenuItem];
NSMenu* m = [mitem menu];
[m cancelTracking];
[m performActionForItemAtIndex: [m indexOfItem: mitem]];
}
But i'm having problems with keyhandling, if you solved this problem maybe you can go to my question and help me a little bit.
Add this to your custom view and you should be fine:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return YES;
}
I added this method to my custom view, and now everything works beautifully:
- (void)viewDidMoveToWindow {
[[self window] becomeKeyWindow];
}
Hope this helps!
I've updated this version for SwiftUI Swift 5.3:
final class HostingView<Content: View>: NSHostingView<Content> {
override func viewDidMoveToWindow() {
window?.becomeKey()
}
}
And then use like so:
let item = NSMenuItem()
let contentView = ContentView()
item.view = HostingView(rootView: contentView)
let menu = NSMenu()
menu.items = [item]
So far, the only way to achieve the goal, is to register a tracking area manually in updateTrackingAreas - that is thankfully called, like this:
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect: bounds, options: [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp], owner: self, userInfo: nil)
addTrackingArea(trackingArea)
}
Recently I needed to show a Custom view for a NSStatusItem, show a regular NSMenu when clicking on it and supporting drag and drop operations on the Status icon.
I solved my problem using, mainly, three different sources that can be found in this question.
Hope it helps other people.
See the sample code from Apple named CustomMenus
In there you'll find a good example in the ImagePickerMenuItemView class.
It's not simple or trivial to make a view in a menu act like a normal NSMenuItem.
There are some real decisions and coding to do.

Is there an equivalent technique in Cocoa for the synchronous TrackPopupMenu in Windows?

In response to a rightMouse event I want to call a function that displays a context menu, runs it, and responds to the selected menu item. In Windows I can use TrackPopupMenu with the TPM_RETURNCMD flag.
What is the easiest way to implement this in Cocoa? It seems NSMenu:popUpContextMenu wants to post an event to the specified NSView. Must I create a dummy view and wait for the event before returning? If so, how do I "wait" or flush events given I am not returning to my main ?
The 'proper' way to do this in Cocoa is to have your menu item's target and action perform the required method. However, if you must do it within your initial call, you can use [NSView nextEventMatchingMask:] to continually fetch new events that interest you, handle them, and loop. Here's an example which just waits until the right mouse button is released. You'll probably want to use a more complex mask argument, and continually call [NSView nextEventMatchingMask:] until you get what you want.
NSEvent *localEvent = [[self window] nextEventMatchingMask: NSRightMouseUpMask];
I think you'll find the 'proper' way to go much easier.
It appears that popUpContextMenu is already synchronous. Since I didn't see a way to use NSMenu without having it send a notification to an NSView I came up with a scheme that instantiates a temporary NSView. The goal is to display a popup menu and return the selected item in the context of a single function call. Following is code snippets of my proposed solution:
// Dummy View class used to receive Menu Events
#interface DVFBaseView : NSView
{
NSMenuItem* nsMenuItem;
}
- (void) OnMenuSelection:(id)sender;
- (NSMenuItem*)MenuItem;
#end
#implementation DVFBaseView
- (NSMenuItem*)MenuItem
{
return nsMenuItem;
}
- (void)OnMenuSelection:(id)sender
{
nsMenuItem = sender;
}
#end
// Calling Code (in response to rightMouseDown event in my main NSView
void HandleRButtonDown (NSPoint pt)
{
NSRect graphicsRect; // contains an origin, width, height
graphicsRect = NSMakeRect(200, 200, 50, 100);
//-----------------------------
// Create Menu and Dummy View
//-----------------------------
nsMenu = [[[NSMenu alloc] initWithTitle:#"Contextual Menu"] autorelease];
nsView = [[[DVFBaseView alloc] initWithFrame:graphicsRect] autorelease];
NSMenuItem* item = [nsMenu addItemWithTitle:#"Menu Item# 1" action:#selector(OnMenuSelection:) keyEquivalent:#""];
[item setTag:ID_FIRST];
item = [nsMenu addItemWithTitle:#"Menu Item #2" action:#selector(OnMenuSelection:) keyEquivalent:#""];
[item setTag:ID_SECOND];
//---------------------------------------------------------------------------------------------
// Providing a valid windowNumber is key in getting the Menu to display in the proper location
//---------------------------------------------------------------------------------------------
int windowNumber = [(NSWindow*)myWindow windowNumber];
NSRect frame = [(NSWindow*)myWindow frame];
NSPoint wp = {pt.x, frame.size.height - pt.y}; // Origin in lower left
NSEvent* event = [NSEvent otherEventWithType:NSApplicationDefined
location:wp
modifierFlags:NSApplicationDefined
timestamp: (NSTimeInterval) 0
windowNumber: windowNumber
context: [NSGraphicsContext currentContext]
subtype:0
data1: 0
data2: 0];
[NSMenu popUpContextMenu:nsMenu withEvent:event forView:nsView];
NSMenuItem* MenuItem = [nsView MenuItem];
switch ([MenuItem tag])
{
case ID_FIRST: HandleFirstCommand(); break;
case ID_SECOND: HandleSecondCommand(); break;
}
}
There is no direct equivalent, except in Carbon, which is deprecated.
For detecting the right-click, follow these instructions. They ensure that you will properly detect right-clicks and right-holds and display the menu when you should and not display it when you shouldn't.
For following events, you might try [[NSRunLoop currentRunLoop] runMode:NSEventTrackingRunLoopMode untilDate:[NSDate distantFuture]]. You will need to call this repeatedly until the user has chosen one of the menu items.
Using nextEventMatchingMask:NSRightMouseUpMask will not work in all, or even most, cases. If the user right-clicks on your control, the right mouse button will go up immediately after it goes down, without selecting a menu item, and the menu item selection will probably (though not necessarily) happen through the left mouse button. Better to just run the run loop repeatedly until the user either selects something or dismisses the menu.
I don't know how to tell that the user has dismissed the menu without selecting anything.

Resources