5

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.

Gustav Larsson
  • 8,199
  • 3
  • 31
  • 51
  • Could you post the code you're using to change the events? I've done something similar and I'll try to dig up mine. – jscs Apr 28 '11 at 01:19

3 Answers3

6

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.

Neha
  • 1,751
  • 14
  • 36
Francis McGrew
  • 7,264
  • 1
  • 33
  • 30
  • Yes! Suddenly everything makes sense. I had to change the point conversion though, I ended up converting the point instead, using `[self convertPoint:[newEvent locationInWindow] fromView:nil]` and then comparing it to `[self bound]`. I also added a `[self highlight:NO]` to after the while loop. Thanks! – Gustav Larsson Apr 28 '11 at 14:37
2

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.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • Unfortunately this didn't make it work for me. I am investigating it further though. – Gustav Larsson Apr 28 '11 at 03:01
  • By the way, I figured out that making the button looked pushed in is done by `-highlight:`. Now I should be able to roll my own, as soon as I figure out how to intercept `-mouseUp:` for NSButton. Thanks for all your help! – Gustav Larsson Apr 28 '11 at 03:57
  • @Gustav: yes, that's weird, `rightMouseUp:` never gets called. – jscs Apr 28 '11 at 04:22
  • @Gustav: added a bit of info that I just figured out about `rightMouseUp:` Hope it's useful! Good luck! – jscs Apr 28 '11 at 04:30
  • 1
    Thanks for some really good investigative work! You were definitely on to it, but the only thing was that it wasn't waiting for a `-mouseUp:`, it was waiting for a mouse up event all on it own. So I kept trying to intercept `-mouseUp:`, without luck. – Gustav Larsson Apr 28 '11 at 14:50
1

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
  }
}
AlexT
  • 596
  • 7
  • 15