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);
}
}
}
Related
My goal is simple and yet I cannot find a solution in spite of lots of searching.
Basically, when my app is in full-screen (kiosk) mode, I want the toolbar only to auto-hide, but I want the menu bar hidden.
Apparently this combination is not valid. I've tried:
- (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions: (NSApplicationPresentationOptions)proposedOptions
{
return (NSApplicationPresentationFullScreen |
NSApplicationPresentationHideDock |
NSApplicationPresentationHideMenuBar |
NSApplicationPresentationAutoHideToolbar);
}
I get the following exception:
"... fullscreen presentation options must include NSApplicationPresentationAutoHideMenuBar if NSApplicationPresentationAutoHideToolbar is included"
Thing is, I don't want the menu bar displayed at all!
So, I'm presuming this is not possible using the standard presentation options. Any ideas how I might approach implementing this behaviour manually?
I'm thinking along the lines of: detect the mouse position and only show/hide the toolbar when the mouse is at/near the top of the screen.
I'm very new to Cocoa so not sure where I would start to achieve this. Any help much appreciated!
Many thanks,
John
I've got It to work, but only by using private APIs.
First I had to find out how to prevent the menubar from appearing. I discovered the functions _HIMenuBarPositionLock and _HIMenuBarPositionUnlock, from Carbon (link the app with Carbon.framework).
Then I had to create a custom subclass of NSToolbar, at awakeFromNib I register notification observers to lock and unlock the menubar when the window enters and exits fullscreen, respectively:
- (void)awakeFromNib
{
[super awakeFromNib];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillEnterFullScreenNotification object:[self _window] queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
// lock menubar position when entering fullscreen so It doesn't appear when the mouse is at the top of the screen
_HIMenuBarPositionLock();
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillExitFullScreenNotification object:[self _window] queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
// unlock menubar position when exiting fullscreen
_HIMenuBarPositionUnlock();
}];
[self _setupToolbarHotspotTrackingView];
}
_setupToolbarHotspotTrackingView is a method on SOToolbar which adds a view to the window, this view will be used to track the mouse location and show/hide the toolbar accordingly.
- (void)_setupToolbarHotspotTrackingView
{
NSView *contentView = [self _window].contentView;
self.toolbarHotspotTrackingView = [[SOToolbarTrackingView alloc] initWithFrame:contentView.bounds];
[contentView addSubview:self.toolbarHotspotTrackingView];
self.toolbarHotspotTrackingView.autoresizingMask = NSViewWidthSizable|NSViewHeightSizable;
self.toolbarHotspotTrackingView.toolbar = self;
}
I also had to override _attachesToMenuBar on SOToolbar so the animation works properly.
- (BOOL)_attachesToMenuBar
{
return NO;
}
SOToolbarTrackingView sets up a tracking area for mouse moved events and checks to see if the mouse is at the top of the window. It then calls some methods on the private class NSToolbarFullScreenWindowManager to show and hide the toolbar.
There's too much stuff to explain It all in detail here, I've uploaded my experimental project so you can take a look. Download the sample project here.
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];
}
I have an NSTableView that contains 2 different Columns - one is an NSImageCell that shows a file icon, and the second is a custom subclass of NSTextFieldCell that contains a quick look button on the right of the text. When I click the Quick Look button, the following code is invoked:
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
This does it's job and shows the blank Quick Look panel saying "No Items Selected." After I did a bit of research on the internet, I implemented a custom NSTableView subclass to be the Delegate and Data Source for the Quick Look panel. I get the notification that Quick Look asks if I want to be the delegate, and I respond with return YES. Even though I implement all methods in both QLPreviewPanelDataSource and QLPreviewPanelDelegate, at runtime I get this error on the console:
2010-12-24 15:32:17.235 BackMeUp[4763:80f] clicked: ~/Desktop/HUDTape.mov
2010-12-24 15:32:17.489 BackMeUp[4763:80f] [QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
2010-12-24 15:32:17.490 BackMeUp[4763:80f] [QL] QLError(): -[QLPreviewPanel setDataSource:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
2010-12-24 15:32:17.491 BackMeUp[4763:80f] We can now receive QL Events.
2010-12-24 15:32:18.291 BackMeUp[4763:80f] -[NSPathStore2 stringValue]: unrecognized selector sent to instance 0x5ecb10
2010-12-24 15:32:18.292 BackMeUp[4763:80f] -[NSPathStore2 stringValue]: unrecognized selector sent to instance 0x5ecb10
And the Quick Look panel does not show up, which I find rather odd. The first line above is just that I know the cell has been clicked. Anyways, here is the .m file of the custom table view subclass:
//
// BackupListTableView.m
// BackMeUp
//
// Created by Tristan Seifert on 12/24/10.
// Copyright 2010 24/7 Server. All rights reserved.
//
#import "BackupListTableView.h"
#implementation BackupListTableView
- (void) awakeFromNib {
}
// Quick Look Delegates
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel;
{
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
NSLog(#"We can now receive QL Events.");
return YES;
}
- (void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document is now responsible of the preview panel
// It is allowed to set the delegate, data source and refresh panel.
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
}
- (void)endPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document loses its responsisibility on the preview panel
// Until the next call to -beginPreviewPanelControl: it must not
// change the panel's delegate, data source or refresh it.
return;
}
// Quick Look panel data source
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel
{
return 1;
}
- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index
{
int selectedRow = [self selectedRow];
return [NSURL URLWithString:[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]];
}
// Quick Look panel delegate
- (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event
{
// redirect all key down events to the table view
return NO;
}
// This delegate method provides the rect on screen from which the panel will zoom.
- (NSRect)previewPanel:(QLPreviewPanel *)panel sourceFrameOnScreenForPreviewItem:(id <QLPreviewItem>)item
{
NSRect iconRect = [self rectOfColumn:1];
/*
// check that the icon rect is visible on screen
NSRect visibleRect = [self visibleRect];
// convert icon rect to screen coordinates
iconRect = [self convertRectToBase:iconRect];
iconRect.origin = [[self window] convertBaseToScreen:iconRect.origin];
*/
return iconRect;
}
// This delegate method provides a transition image between the table view and the preview panel
- (id)previewPanel:(QLPreviewPanel *)panel transitionImageForPreviewItem:(id <QLPreviewItem>)item contentRect:(NSRect *)contentRect
{
int selectedRow = [self selectedRow];
NSImage *fileIcon = [[NSWorkspace sharedWorkspace] iconForFile:[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]];
return fileIcon;
}
#end
Thanks for any help.
The documentation isn't the best for this, since it's a new feature that was added in 10.6. (Well, there is obviously the class and protocol references, but in my experience, I've always found the Companion Guides to be more helpful in understanding how the objects are intended to be used in a real-world scenario).
The QLPreviewPanelController Protocol Reference defines 3 methods:
QLPreviewPanelController Protocol Reference
The Quick Look preview panel shows previews for items provided by the first object in the responder chain that implements the methods in this protocol. You typically implement these methods in your window controller or delegate. You should never try to modify preview panel state if you’re not controlling the panel.
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel;
- (BOOL)beginPreviewPanelControl:(QLPreviewPanel *)panel;
- (void)endPreviewPanelControl:(QLPreviewPanel *)panel;
I'm guessing that your code should look like this:
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
{
return YES;
}
You shouldn't be doing anything in that method besides returning YES. acceptsPreviewPanelControl: is sent to every object in the responder chain until something returns YES. By returning YES, that object effectively becomes "the controller". The latter 2 methods are called on the controller object after it returns YES from the first method. So you should only be setting the delegate and datasource in the beginPreviewPanelControl: method (at which time you will be regarded as the current controller).
- (void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document is now responsible of the preview panel
// It is allowed to set the delegate, data source and refresh panel.
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
NSLog(#"We can now receive QL Events.");
}
First:
-acceptsPreviewPanelControl should only return YES and not try to set delegate and datasource.
Then, the problem is that you get an exception breaking the panel:
2010-12-24 15:32:18.291 BackMeUp[4763:80f] -[NSPathStore2 stringValue]: unrecognized selector sent to instance 0x5ecb10
The exception is very likely caused by these invocations:
[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]
Very likely, [[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] is a file path (a NSPathStore instance which is a subclass of NSString) so it does not respond to -stringValue
So replace:
[NSURL URLWithString:[[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow] stringValue]]
by:
[NSURL fileURLWithPath:[[self dataSource] tableView:self objectValueForTableColumn:fileColumn row:selectedRow]]
Also remove the call to -stringValue in transitionImageForPreviewItem.
As a side note, it seems to be suboptimal to load the image at each call of transitionImageForPreviewItem. I suppose you already have the image computed elsewhere (displayed in your table view), try to use it.
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.
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.