Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSButton subclass that responds to right clicks

Tags:

cocoa

nsbutton

I have an NSButton subclass that I would like to make work with right mouse button clicks. Just overloading -rightMouseDown: won't cut it, as I would like the same kind of behaviour as for regular clicks (e.g. the button is pushed down, the user can cancel by leaving the button, the action is sent when the mouse is released, etc.).

What I have tried so far is overloading -rightMouse{Down,Up,Dragged}, changing the events to indicate the left mouse button clicks and then sending it to -mouse{Down,Up,Dragged}. Now this would clearly be a hack at best, and as it turns out Mac OS X did not like it all. I can click the button, but upon release, the button remains pushed in.

I could mimic the behaviour myself, which shouldn't be too complicated. However, I don't know how to make the button look pushed in.


Before you say "Don't! It's an unconventional Mac OS X behaviour and should be avoided": I have considered this and a right click could vastly improve the workflow. Basically the button cycles through 4 states, and I would like a right click to make it cycle in reverse. It's not an essential feature, but it would be nice. If you still feel like saying "Don't!", then let me know your thoughts. I appreciate it!

Thanks!

EDIT: This was my attempt of changing the event (you can't change the type, so I made a new one, copying all information across. I mean, I know this is the framework clearly telling me Don't Do This, but I gave it a go, as you do):

// I've contracted all three for brevity
- (void)rightMouse{Down,Up,Dragging}:(NSEvent *)theEvent {
    NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouse{Down,Up,Dragging} location:[theEvent locationInWindow] modifierFlags:[theEvent modifierFlags] timestamp:[theEvent timestamp] windowNumber:[theEvent windowNumber] context:[theEvent context] eventNumber:[theEvent eventNumber] clickCount:[theEvent clickCount] pressure:[theEvent pressure]];
    [self mouse{Down,Up,Dragging}:event]; 
}

UPDATE: I noticed that -mouseUp: was never sent to NSButton, and if I changed it to an NSControl, it was. I couldn't figure out why this was, until Francis McGrew pointed out that it contains its own event handling loop. Now, this also made sense to why before I could reroute the -rightMouseDown:, but the button wouldn't go up on release. This is because it was fetching new events on its own, that I couldn't intercept and convert from right to left mouse button events.

like image 991
Gustav Larsson Avatar asked Apr 28 '11 00:04

Gustav Larsson


3 Answers

I've turned a left mouse click-and-hold into a fake right mouse down on a path control. I'm not sure this will solve all your problems, but I found that the key difference when I did this was changing the timestamp:

NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouseDown 
                                    location:[theEvent locationInWindow] 
                               modifierFlags:[theEvent modifierFlags] 
                                   timestamp:CFAbsoluteGetTimeCurrent()
                                windowNumber:[theEvent windowNumber] 
                                     context:[theEvent context]
 // I was surprised to find eventNumber didn't seem to need to be faked 
                                 eventNumber:[theEvent eventNumber]   
                                  clickCount:[theEvent clickCount] 
                                    pressure:[theEvent pressure]];

The other thing is that depending on your button type, its state may be the value that is making it appear pushed or not, so you might trying poking at that.

UPDATE: I think I've figured out why rightMouseUp: never gets called. Per the -[NSControl mouseDown:] docs, the button starts tracking the mouse when it gets a mouseDown event, and it doesn't stop tracking until it gets mouseUp. While it's tracking, it can't do anything else. I just tried, for example, at the end of a custom mouseDown::

[self performSelector:@selector(mouseUp:) withObject:myFakeMouseUpEvent afterDelay:1.0];

but this gets put off until a normal mouseUp: gets triggered some other way. So, if you've clicked the right mouse button, you can't (with the mouse) send a leftMouseUp, thus the button is still tracking, and won't accept a rightMouseUp event. I still don't know what the solution is, but I figured that would be useful information.

like image 34
jscs Avatar answered Oct 11 '22 03:10

jscs


NSButton is entering a mouse tracking loop. To change this you will have to subclass NSButton and create your own custom tracking loop. Try this code:

- (void) rightMouseDown:(NSEvent *)theEvent 
{
    NSEvent *newEvent = theEvent;
    BOOL mouseInBounds = NO;

    while (YES) 
        {
            mouseInBounds = NSPointInRect([newEvent locationInWindow], [self convertRect:[self frame] fromView:nil]);
            [self highlight:mouseInBounds];

            newEvent = [[self window] nextEventMatchingMask:NSRightMouseDraggedMask | NSRightMouseUpMask];

            if (NSRightMouseUp == [newEvent type])
            {
                break;
            }
        }
    if (mouseInBounds) [self performClick:nil];
}

This is how I do it; Hopefully it will work for you.

like image 155
Francis McGrew Avatar answered Oct 11 '22 04:10

Francis McGrew


Not much to add to the answers above, but for those working in Swift, you may have trouble finding the constants for the event mask, buried deep in the documentation, and still more trouble finding a way to combine (OR) them in a way that the compiler accepts, so this may save you some time. Is there a neater way? This goes in your subclass -

var rightAction: Selector = nil 
  // add a new property, by analogy with action property

override func rightMouseDown(var theEvent: NSEvent!) {
  var newEvent: NSEvent!

  let maskUp = NSEventMask.RightMouseUpMask.rawValue
  let maskDragged = NSEventMask.RightMouseDraggedMask.rawValue
  let mask = Int( maskUp | maskDragged ) // cast from UInt

  do {
    newEvent = window!.nextEventMatchingMask(mask)
  }
  while newEvent.type == .RightMouseDragged

My loop has become a do..while, as it always has to execute at least once, and I never liked writing while true, and I don't need to do anything with the dragging events.

I had endless trouble getting meaningful results from convertRect(), perhaps because my controls were embedded in a table view. Thanks to Gustav Larsson above for my ending up with this for the last part -

let whereUp = newEvent.locationInWindow
let p = convertPoint(whereUp, fromView: nil)
let mouseInBounds = NSMouseInRect(p, bounds, flipped) // bounds, not frame
if mouseInBounds {
  sendAction(rightAction, to: target)
  // assuming rightAction and target have been allocated
  }
}
like image 36
AlexT Avatar answered Oct 11 '22 03:10

AlexT