I have a transient NSPopover and when it's open and I click a button in the parent window the popover is dismissed instead and the click "swallowed". Only the second click on the button triggers the action properly.
Is there a way to pass the first click through to the control directly and dismiss the popover in one step?
NSPopover does seem to swallow the event for the targeted position view. Other views are fine. My solution is to get the delegate to forward the last mouse down event to the target view if hit testing reveals it was the clicked view. Unfortunatety NSApp -currentEvent is nil when the delegate gets messaged - not sure why. So I added an event monitor to the app delegate like so:
- (void)addEventMonitor
{
if (self.eventMonitor) {
return;
}
self.eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSLeftMouseDownMask) handler:^(NSEvent *incomingEvent) {
NSEvent *result = incomingEvent;
self.monitoredEvent = result;
return result;
}];
}
When the delegate closes it checks to see it the last monitored event was in the target:
- (void)popoverDidClose:(NSNotification *)notification
{
// [NSApp currentEvent] is nil here
NSEvent *event = [(BPApplicationDelegate *)[NSApp delegate] monitoredEvent];
if (event && (event.type & NSLeftMouseDown)) {
NSPoint pt = [self.targetView.superview convertPoint:event.locationInWindow fromView:nil];
if ([self.TargetView hitTest:pt]) {
[NSApp postEvent:event atStart:NO];
}
}
}
Related
I have mouse with 5 buttons. They are left, right, wheel and 2 side buttons (back/forward). Is there any way to check, whether side button ("back" for example) was pressed? I learning cocoa's documentation about mouse event. At the moment I know that there is - (void)otherMouseDown:(NSEvent *)event; method, which is called whenever other (not right/left) button was pressed. But documentation tells nothing about additional information about which button was actually pressed. How to overcome this?
-(void)scrollWheel:(NSEvent *)event; method of NSResponder and so also from NSViews
- (void)rightMouseDown:(NSEvent *)event; same way
- (void)otherMouseDown:(NSEvent *)event as you know
but also..
-(void)someAction:(id)sender
{
NSEvent *event = [NSApp currentEvent];
if (event.type == NSEventTypeLeftMouseUp ||
event.type == NSEventTypeLeftMouseDown)
{
if (event.clickCount == 2) {
// doubleClick
}
}
else if (event.type == NSEventTypeOtherMouseDown)
{
if (event.buttonNumber==4)
{
//i think first button is 0, so 5 should be 4.
}
}
}
AppKit's NSEvent.h has everything you need just check the type of the event first and then go for buttonNumber and what ever you choose
I have NSAlert instance which i run as modal for user confirmation on cancelling some operation. When user doesn't respond and the operations gets completed , i need to close this modal window. So, for this i'm calling performClick on default button in alert. But i observe that the perform click doesnt get executed instantly but waits for some external event such as mouse move event. Why is this happening? Apart from posting fake event, what are the other solutions?
Here is what you need to do.
Assumption:
1. IBAction is connect to NSButton Which will display the Alert View after clicking upon it.
2. It will perform Click operation by itself on the Second button of the Alert View.
Hope the below code will help you....
- (IBAction)showAlert:(id)sender
{
//display the alert
self.myAlert = [NSAlert alertWithMessageText:#"Sample Test" defaultButton:#"OK" alternateButton:#"DO Nothing" otherButton:#"CANCEL" informativeTextWithFormat:#"TEST",nil];
[self.myAlert beginSheetModalForWindow:[self window]
modalDelegate:self
didEndSelector:#selector(errorAlertDidEnd:returnCode:contextInfo:)
contextInfo:nil];
NSArray *buttonArray = [self.myAlert buttons];
NSLog(#"Button Arrays %#",buttonArray);
//Close by itself without a mouse click by the user
//Assuming the Default Button as the Second one "Do Nothing
NSButton *myBtn = [buttonArray objectAtIndex:2];
[myBtn performClick:self.myAlert];
}
- (void)errorAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
NSLog(#"TEST");
}
To know which button is clicked you can modify the mTo know which button is clicked you can modify the method errorAltertDidEnd
- (void)errorAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
if(returnCode == NSAlertAlternateReturn)
{
NSLog(#"TEST Alternate %ld",returnCode);
}
if(returnCode == NSAlertDefaultReturn)
{
NSLog(#"TEST Default %ld",returnCode);
}
if(returnCode == NSAlertOtherReturn)
{
NSLog(#"Test Other %ld",returnCode);
}
}
Could you please elaborate on this "But the click event(generated from performClick) itself waits for some external event(example: mouse move) –"
How can I determine if a child window has been moved directly by the user rather than as a result of its parent window being moved?
I get NSWindowDidMoveNotification notifications in both cases.
I figured I could test the parent frame origin in the NSWindowWillMoveNotification handler and compare it against the origin when in the NSWindowDidMoveNotification handler, but is there a better way perhaps?
Here is my current solution:
- (void)windowWillMove:(NSNotification *)notification
{
NSWindow *window = [notification object];
_parentOrigin = [[window parentWindow] frame].origin;
}
- (void)windowDidMove:(NSNotification *)notification
{
NSWindow *window = [notification object];
NSPoint newParentOrigin = [[window parentWindow] frame].origin;
if (_parentOrigin.x == newParentOrigin.x &&
_parentOrigin.y == newParentOrigin.y)
{
// The parent hasn't moved, therefore the user moved the window directly...
[window doThing];
}
}
The window that you click on to move will become the key window, while the child window doesn't become key if it's moved as a result of moving the parent window. So, if you test for whether the window sending the notification is the key window, you can tell which was moved by the user.
- (void)windowDidMove:(NSNotification *)notification {
NSWindow *movedWindow = notification.object;
if ([movedWindow isKeyWindow]) {
NSLog(#"%#",movedWindow);
[movedWindow doThing];
}
}
I have a view with a long press gesture recognizer in it:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressDetected:)];
[self addGestureRecognizer:longPress];
[longPress release];
}
return self;
}
When the long press is detected, I want to show a UIMenuViewController above the view with a single action in it, and when that menu item is tapped I want to execute a block:
- (void)longPressDetected:(UILongPressGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self becomeFirstResponder];
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *actionItem = [[UIMenuItem alloc] initWithTitle:#"Action" action:#selector(someActionSelector)];
[menuController setMenuItems:[NSArray arrayWithObject:actionItem]];
[actionItem release];
[menuController setTargetRect:self.frame inView:self.superview];
[menuController setMenuVisible:YES animated:YES];
}
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == #selector(copy:) || action == #selector(cut:) || action == #selector(delete:) ||
action == #selector(paste:) || action == #selector(select:) || action == #selector(selectAll:)) {
return NO;
}
else if (action == #selector(someActionSelector)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}
- (void)someActionSelector {
if (self.actionBlock) {
self.actionBlock();
}
}
Problem is, this only works after the second long-press and tap combo. The first time I long-press on the view I see the menu, but tapping the menu does nothing. The second time I see the menu again, I tap it, then the block is executed.
Debugger shows that a breakpoint in someActionSelector is only reached on the second tap. Any idea why this is?
I figured it out. The view listening for a long press is contained inside a view that repositions some subviews when its frame is changed (by overriding setFrame:, which seems like a bad idea but I couldn't think of another way). So when the long press happened it triggered a layoutSubviews in the parent of the parent of the listening view, which set the frame of the parent of the listening view, which repositioned the listening view, which appears to break either the responder chain or deactivate the menu. The solution was to add a condition inside the overridden setFrame: to only trigger the layout if the frame actually changes, which it doesn't on the long press. I'm sure there's a better alternative for listening to frame changes that would avoid this problem entirely—feel free to suggest them in comments.
How should I go about it?
I was thinking about...
[NSMenu popUpContextMenu:menu withEvent:event forView:(NSButton *)sender];
Yup.
On button action call
[NSMenu popUpContextMenu:menu withEvent:event forView:(NSButton *)sender];
where
menu : menu you want to show
sender : button you clicked
event : a new NSEvent you create
When you create the new NSEvent, specify the location as to where you want the popup menu to be shown.
Swift version of accepted answer
#IBAction func actionOccurred(sender: NSButton) {
if let event = NSApplication.sharedApplication().currentEvent {
NSMenu.popUpContextMenu(sender.menu!, withEvent: event, forView: sender)
}
}
Updated answer
Add NSMenu to the NSViewController in the storyboard as seen in the image
Connect the Outlet
Right-click on the button in the storyboard and connect the menu outlet to the menu that we have just added to the storyboard(Where arrow points in the image)
Add IBAction
#IBAction func menuClicked(_ sender: NSButton) {
var location = NSEvent.mouseLocation
location.x -= 10; location.y -= 10 // Menu appears below the button
let menu = sender.menu!
menu.popUp(positioning: menu.item(at: 0), at: location, in: nil)
}
As I have commented I find the ButtonMadness example less than perfect.
My implementation seems to work better. The menu is shown on mouse down, the button remains depressed throughout, the menu position can be specified and the menu is dismissed without subsequent spurious display.
To be honest NSPopupButton is a better choice in the majority of situations. I use this code mainly because of the convenience of having one class for buttons and popups and because the menu does not contain the popup control image and title. I load the menu from a separate nib and reuse it as is elsewhere in the app as required.
Note that it is trivial to add additional support for say a popover as well as menu.
NSButton subclass:
- (void)mouseDown:(NSEvent *)theEvent {
// if a menu is defined let the cell handle its display
if (self.menu) {
if ([theEvent type] == NSLeftMouseDown) {
[[self cell] setMenu:[self menu]];
} else {
[[self cell] setMenu:nil];
}
}
[super mouseDown:theEvent];
}
NSButtonCell subclass:
- (BOOL)trackMouse:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
// if menu defined show on left mouse
if ([event type] == NSLeftMouseDown && [self menu]) {
NSPoint result = [controlView convertPoint:NSMakePoint(NSMidX(cellFrame), NSMidY(cellFrame)) toView:nil];
NSEvent *newEvent = [NSEvent mouseEventWithType: [event type]
location: result
modifierFlags: [event modifierFlags]
timestamp: [event timestamp]
windowNumber: [event windowNumber]
context: [event context]
eventNumber: [event eventNumber]
clickCount: [event clickCount]
pressure: [event pressure]];
// need to generate a new event otherwise selection of button
// after menu display fails
[NSMenu popUpContextMenu:[self menu] withEvent:newEvent forView:controlView];
return YES;
}
return [super trackMouse:event inRect:cellFrame ofView:controlView untilMouseUp:untilMouseUp];
}
Recently, I was trying to implement it and I came, as I think, with a simpler solution
-(IBAction)buttonClick:(id)sender {
NSButton * b = (NSButton*)sender;
NSPoint l = [ self.window convertBaseToScreen:b.frame.origin ];
[ self.menu popUpMenuPositioningItem:nil atLocation:l inView:nil ];
}
Update
convertBaseToScreen is deprecated starting from 10.7, instead of it use convertRectToScreen in the following way:
NSPoint l = [self.window convertRectToScreen:b.frame].origin;
Using a context menu on the action call isn't a great way to do it because the menu doesn't show until mouseUp - you don't get the hold & drag menu behavior. Apple's ButtonMadness sample shows how to really do this in a subclass of NSButton, see DropDownButton. https://developer.apple.com/library/mac/samplecode/ButtonMadness/Introduction/Intro.html
Summarizing that subclass: create a NSPopUpButtonCell with pullsDown set to YES & preferredEdge to NSMaxYEdge, copy your menu to add a blank top item and set it as that cell's menu, on mouseDown call [thePopUpCell performClickWithFrame:self.bounds inView:self] and set self.needsDisplay
Do like that.
-(IBAction)onClickSourceAdd:(id)sender {
NSMenu *mainMenu = [NSApp mainMenu];
NSMenu *sourceMenu = [[mainMenu itemAtIndex:2] submenu];
NSMenu *addMenu = [[sourceMenu itemAtIndex:0] submenu];
[NSMenu popUpContextMenu:addMenu
withEvent:[NSApp currentEvent]
forView:(NSButton *)sender];
}