NSComboBoxCell eats mouseUp events - macos

I have an NSComboBox child of an NSPopover. The popover is transient and is configured to dismiss when the user clicks outside its bounds.
When its combobox popup is active and displayed, and when the user mouses down on the popovers owning view, the view receives the mouse down as expected, the popover disappears and the combobox dismisses, however the very next mouseUp is never received by the view.
It turns out the NSComboBoxCell's trackMouse method (tracking loop) is not returning until it receives a mouseUp but unlike the case of mouseDown where it redispatches it nicely to the view that was clicked on, it never propagates the mouseUp.
I had to work around the issue with the following NSComboBoxCell trackMouse override.
Has anyone seen this issue prior or understand what may be going on?
--------
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame
ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp {
BOOL trackingResult = [super trackMouse:theEvent inRect:cellFrame
ofView:controlView untilMouseUp:untilMouseUp];
// If we were dismissed due to mouse event and the current
// event (that NSComboBoxCell is currently eating), just redispatch
// it so it lands with its intended destination.
if (trackingResult) {
NSEvent* currentEvent = [NSApp currentEvent];
if (currentEvent.type == NSLeftMouseUp && currentEvent.window != nil) {
[NSApp postEvent:currentEvent atStart:YES];
}
}
return trackingResult;
}

Related

NSTrackingArea not fully working when not key window

A bit of context first. Essentially I have a window that is covering the desktop. On it I have a few WebKit WebView views which allow user interaction. By deafult, as one would expect, when another application is active it does not receive these events (such as hovering, mouse entered, and clicking). I can make it work by clicking my window first, then moving the mouse, but this is not good for usability. I've also managed to make it activate the window when the cursor enters, but it's far from ideal and rather hacky.
So instead I'm trying to use a tracking area. At the moment on the WebViews superview I have this tracking area:
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self visibleRect]
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveAlways
owner:self
userInfo:nil];
This works as I want it to, I'm receiving the all the mouse events. However, the WebViews don't seem to be responding as intended. JavaScript mouse move events only fire when I hold and drag, not just hover and drag.
I've tried using hitTest to get the correct view, but nothing seems to work. Here's an example method, I'm using the isHandlingMouse boolean because without it an infinite loop seemed to be created for some reason:
- (NSView *)handleTrackedMouseEvent: (NSEvent *)theEvent{
if(isHandlingMouse)
return nil;
isHandlingMouse = true;
NSView *hit = [self hitTest: theEvent.locationInWindow];
if (hit && hit != self) {
return hit;
}
return nil;
}
- (void)mouseMoved:(NSEvent *)theEvent{
NSView *hit = [self handleTrackedMouseEvent: theEvent];
if (hit){
[hit mouseMoved: theEvent];
}
isHandlingMouse = false;
}
The 'hit' view, is a WebHTMLView, which appears to be a private class. Everything seems like it should be working,but perhaps there's something I'm doing that's breaking it, or I'm sending the event to the WebHTMLView incorrectly.
Post a sample Xcode project to make it easier for people to test solutions to this problem.
I was doing something similar and it took a lot of trial and error to find a solution. You will likely need to subclass NSWindow and add - (BOOL)canBecomeKeyWindow { return YES; }, then whenever you detect the mouse is over your window, you might call [window orderFrontRegardless] just so it can properly capture the mouse events.

Changing the selection behaviour of NSCollectionView

In my Mac app I have a NSCollectionView with multi select enabled. In my app being able to select more than one item is the norm, and having to press cmd while clicking to select multiple items is frustrating some users and most don't realise they can do it (I get a lot of feature requests asking for multi select).
So, I want to change the behaviour so that:
when a user clicks a second item, the first item remains selected (without the need for holding cmd)
When a user clicks a selected item, the item is deselected
I've tried overriding setSelected on my own subclass of NSCollectionViewItem like so:
-(void)setSelected:(BOOL)flag
{
[super setSelected:flag];
[(MyView*)[self view] setSelected: flag];
[(MyView*)[self view] setNeedsDisplay:YES];
}
Calling super setSelected is required to make sure the collection view functions correctly, but it also seems to be what is responsible for the default behaviour.
What should I do instead?
You could try intercepting all left-mouse-down events using a local events monitor. Within this block you'd then work out if the click happened on your collection view. If it did, create a new event which mimics the event you intercepted but add in the command key mask if it isn't already present. Then, at the end of the block return your event rather than the one you intercepted. Your collection view will behave as if the user had pressed the command key, even though they haven't!
I had a quick go with this in a very simple demo app and it looks like a promising approach - though I expect you'll have to negotiate a few gotchas along the way.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskFromType(NSLeftMouseDown)
handler:^NSEvent *(NSEvent *originalEvent) {
// Did this left down event occur on your collection view?
// If it did add in the command key
NSEvent *newEvent =
[NSEvent
mouseEventWithType: NSLeftMouseDown
location: originalEvent.locationInWindow
modifierFlags: NSCommandKeyMask // I'm assuming it's not already present
timestamp: originalEvent.timestamp
windowNumber: originalEvent.windowNumber
context: originalEvent.context
eventNumber: originalEvent.eventNumber
clickCount: originalEvent.clickCount
pressure:0];
return newEvent; // or originalEvent if it's nothing to do with your collection view
}];
}
Edit (by question author):
This solution is so heavily based on the original answer that this answer deserves credit (feel free to edit)
You can also intercept the mouse event by subclassing the NSCollectionView class and overriding mousedown like this:
#implementation MyCollectionView
-(void) mouseDown:(NSEvent *)originalEvent {
NSEvent *mouseEventWithCmd =
[NSEvent
mouseEventWithType: originalEvent.type
location: originalEvent.locationInWindow
modifierFlags: NSCommandKeyMask
timestamp: originalEvent.timestamp
windowNumber: originalEvent.windowNumber
context: originalEvent.context
eventNumber: originalEvent.eventNumber
clickCount: originalEvent.clickCount
pressure: originalEvent.pressure];
[super mouseDown: mouseEventWithCmd];
}
#end

How to stop NSCollectioView's selectionIndexes from notifying on both mouseDown and mouseUp

I'm observing the 'selectionIndexes' of an NSCollectionView instance like so:
[self.collectionView addObserver:self forKeyPath:#"selectionIndexes" options:0 context:nil];
Then I'm responding to the changes in observeValueForKeyPath: like so:
if(object == self.collectionView)
{
if([keyPath isEqualToString:#"selectionIndexes"])
NSLog(#"selectionIndexes CHANGED");
}
I'm noticing that if I click on an item I get two notifications, one for the mouse down event, and again for the mouse up event. Both times the selectionIndexes property is returning the item's index. You can even do the click slowly, down-pause-up and watch the two distinct firings in the logs.
This doesn't seem normal, how do I stop this? Is there a better way to be notified when an NSCollectionItem is clicked?
UPDATE:
I discovered it is fired twice because I was also clearing the selection in observeValueForKeyPath:
if(object == self.collectionView)
{
if([keyPath isEqualToString:#"selectionIndexes"])
{
NSLog(#"selectionIndexes CHANGED");
[self.collectionView setSelectionIndexes:nil];
}
}
For some reason NSCollectionView sees that it's selection is set to nil in the mouse up event from the item, and it resets the selection indexes. Still not sure how I want to get around this because I do need to set the selection back to zero.
To set an empty selection, set selectionIndexes to an empty NSIndexSet like this:
[self.collectionView setSelectionIndexes:[NSIndexSet indexSet]];

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