SDL icon indicating copy to clipboard operation
SDL copied to clipboard

MacOS opening native popup menu leaving invalid mouse state

Open Victorious3 opened this issue 6 months ago • 3 comments

I'm writing a python app using Kivy, which uses SDL as its backend. I decided it would be useful to show a native context menu on macos instead of rendering my own, and was pretty delighted by how easy this was to achieve, even from python using pyobjc.

There's however a problem with my approach, it seems like macos doesn't send a mouseup event when opening the menu on mousedown (which seems to be common practice on macos). This confuses SDL and causes it to ignore the next mouse press. I have checked the native event queue and it does send a correct "mousedown" event after the menu closes, but it seems like SDL is ignoring the button state that it sends and instead uses its own internal state to track this.

I realize that using such contrived methods with python is probably out of the scope for this project. Therefore I have crafted a minimum example using ObjC to illustrate the issue:

#include <unistd.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_events.h>

#include <AppKit/NSMenu.h>

@interface MenuHandler : NSObject
@end

@implementation MenuHandler
- (void)itemClicked:(id)sender {
    SDL_Log("Clicked!");
}
@end

void open_context_menu(void) {
    NSMenu *menu = [[NSMenu alloc] initWithTitle:@"My Menu"];
    MenuHandler *handler = [[MenuHandler alloc] init];
    NSMenuItem *item1 = [[NSMenuItem alloc] initWithTitle:@"Item 1" action:@selector(itemClicked:) keyEquivalent:@""];
    [item1 setTarget:handler];
    [menu addItem:item1];
    [menu popUpMenuPositioningItem:nil atLocation:NSEvent.mouseLocation inView:nil];
}

int main(int argc, char *argv[]) {
    sleep(1);
    
    SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE);
    if (!SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }

    SDL_Window *window = SDL_CreateWindow("SDL3 Window", 800, 600, SDL_WINDOW_OPENGL);
    if (window == NULL) {
        SDL_Log("Unable to create Window: %s", SDL_GetError());
        return 1;
    }
    
    bool quit = false;
    while (!quit) {
        SDL_Event e;
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_EVENT_QUIT) {
                quit = true;
            }
            if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
                if (e.button.button == SDL_BUTTON_RIGHT) {
                    open_context_menu();
                    float x; float y;
                    int buttons = SDL_GetMouseState(&x, &y);
                    SDL_Log("left: %d, right: %d, middle: %d", (buttons & SDL_BUTTON_LMASK) != 0, (buttons & SDL_BUTTON_RMASK) != 0, (buttons & SDL_BUTTON_MMASK) != 0);
                }
            }
        }
    }
    
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

The menu opens on the first right click while holding the right mouse button down and then dismisses on releasing the button. This incorrectly reports the right mouse button as being pressed after the menu is closed. The next right click doesn't trigger the menu, and then the mouse state fixes itself. The next press works again.

I actually went over triggering a mouse up event from my code after the menu closes and that has fixed the situation. To do this I need accessibility privileges on my app tho, which I would like to avoid.

I think the fix would be to listen to the button state that the event reports instead of tracking this manually. I could try and come up with a PR, if SDL wants to support this use case.

If there's a way to work around this problem somehow I'd love to know a solution!

Victorious3 avatar May 27 '25 19:05 Victorious3

I've looked into the code for a bit and I found this:

https://github.com/libsdl-org/SDL/blob/main/src/events/SDL_mouse.c#L965-L968

I suspect its going to eat the "duplicate" mouse down right there. I don't know if removing this code would break programs.

An alternative could be to directly read the mouse state from the system instead of tracking it manually, at least for macos.

Victorious3 avatar May 28 '25 11:05 Victorious3

I think the least invasive option would be to compare sdl's mouse state to what macos says (CGEventSourceButtonState) and send the missing mouse up event before the next button press. I don't know how strong SDL's guarantee is that these events must always come in pairs, but this change would respect that.

Victorious3 avatar May 31 '25 10:05 Victorious3

I think the least invasive option would be to compare sdl's mouse state to what macos says (CGEventSourceButtonState) and send the missing mouse up event before the next button press. I don't know how strong SDL's guarantee is that these events must always come in pairs, but this change would respect that.

Yes, this seems like a reasonable approach.

slouken avatar May 31 '25 15:05 slouken

This is potentially complicated by having event sources in the future and really wanting a mouse press canceled event in this case, but this will take care of the mouse button being stuck down indefinitely. Please let me know if you have any issues!

slouken avatar Nov 20 '25 21:11 slouken