Skip to content

x11: Read mouse settings at startup so the first click is not reordered - #242

Open
probonopd wants to merge 1 commit into
gnustep:masterfrom
probonopd:fix-first-click-order
Open

probonopd wants to merge 1 commit into
gnustep:masterfrom
probonopd:fix-first-click-order

Conversation

@probonopd

@probonopd probonopd commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Right after an application starts, a quick click on a table row highlights the row but does not select it: no tableViewSelectionDidChange:, no action. Only the second click works. Slow clicks (button held for ~150 ms) are not affected.

Cause

The first ButtonPress calls -initializeMouse from -processEvent:, before the mouse-down event is queued. -initializeMouse registers with NSDistributedNotificationCenter, which waits for gdnc's reply in a nested run loop (NSConnectionReplyMode). The X event source is also active in that mode, so the nested loop processes the pending ButtonRelease and queues the mouse-up first. When -processEvent: returns, the mouse-down is appended after it, so the queue holds [mouse-up, mouse-down]. The table sees the mouse-down last and waits in its tracking loop for a mouse-up that was already delivered.

Backtrace of the ButtonRelease being processed inside the press (abridged):

-[XGServer(EventOps) processEvent:]              ButtonRelease
-[XGServer(EventOps) receivedEvent:type:extra:forMode:]
-[GSRunLoopCtxt pollUntil:within:]
-[NSRunLoop runMode:beforeDate:]                 NSConnectionReplyMode
-[GSMessageHandle sendMessage:beforeDate:]
-[NSConnection(Private) _sendOutRmc:type:sequence:]
...
-[NSDistributedNotificationCenter addObserver:selector:name:object:suspensionBehavior:]
-[XGServer(EventOps) initializeMouse]
-[XGServer(EventOps) processEvent:]              ButtonPress

Fix

Call -initializeMouse once from -initWithAttributes: instead of lazily on the first button press, so no press has to wait for another process.

How to reproduce

  1. Save this as firstclick.m:
#import <AppKit/AppKit.h>

@interface Controller : NSObject
@end

@implementation Controller
- (NSInteger) numberOfRowsInTableView: (NSTableView *)tv
{
  return 5;
}
- (id) tableView: (NSTableView *)tv
objectValueForTableColumn: (NSTableColumn *)col
             row: (NSInteger)row
{
  return [NSString stringWithFormat: @"Row %ld", (long)row];
}
- (void) tableViewSelectionDidChange: (NSNotification *)n
{
  fprintf(stderr, "selected row %ld\n", (long)[[n object] selectedRow]);
}
- (void) applicationDidFinishLaunching: (NSNotification *)n
{
  NSWindow *w = [[NSWindow alloc]
    initWithContentRect: NSMakeRect(100, 100, 300, 200)
              styleMask: NSTitledWindowMask
                backing: NSBackingStoreBuffered
                  defer: NO];
  NSScrollView *sv = [[NSScrollView alloc] initWithFrame: [[w contentView] bounds]];
  NSTableView *tv = [[NSTableView alloc] initWithFrame: [[w contentView] bounds]];
  NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier: @"c"];

  [col setWidth: 280];
  [tv addTableColumn: col];
  [tv setHeaderView: nil];
  [tv setDataSource: self];
  [tv setDelegate: self];
  [sv setDocumentView: tv];
  [[w contentView] addSubview: sv];
  [w makeKeyAndOrderFront: nil];
  fprintf(stderr, "ready\n");
}
@end

int main(int argc, const char **argv)
{
  NSAutoreleasePool *pool = [NSAutoreleasePool new];
  NSApplication *app = [NSApplication sharedApplication];

  [app setDelegate: [Controller new]];
  [app run];
  [pool release];
  return 0;
}
  1. Build it:
. /usr/GNUstep/System/Library/Makefiles/GNUstep.sh   # or wherever GNUstep.sh lives
clang $(gnustep-config --objc-flags) firstclick.m -o firstclick $(gnustep-config --gui-libs)
  1. Start it, and within a few seconds of ready being printed click "Row 1" once with a very short press. xdotool produces such a click reliably (press and release arrive together); a fast real click does the same:
./firstclick &
sleep 5
xdotool mousemove <x of Row 1> <y of Row 1> click 1
sleep 1
xdotool click 1

Without this patch:

ready
selected row 1      <- only after the second click

With this patch:

ready
selected row 1      <- after the first click

The bug needs gdnc to be involved, i.e. the first ButtonPress must trigger the distributed notification registration, so always test on a freshly started process.

Tested on Linux (X11, cairo backend): in repeated runs the first click was lost every time without the patch and never with it. With the patch, double-click detection and the mouse defaults (GSDoubleClickTime, GSMouseMoveThreshold, GSMouseScrollMultiplier, GSMenuButtonEnabled/GSMenuButtonEvent) are read at startup and still follow later changes through the same NSUserDefaultsDidChangeNotification observer.

cc @pkgdemon

The first button press after an application started called
-initializeMouse, which registers with the distributed notification
center.  That registration waits for gdnc in a nested run loop, which
reads the pending ButtonRelease and queues the mouse-up before the
press has queued its mouse-down.  A table then highlighted the clicked
row but never selected it, and the selection only worked on the second
click.  Read the settings and register once in -initWithAttributes:
instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant