From 73b31bfcb42921f08b07659c0861754f7b81866c Mon Sep 17 00:00:00 2001 From: Federico Jeanne Date: Wed, 2 Sep 2026 16:41:41 +0200 Subject: [PATCH] GTK/Cocoa: give synthetic events a fresh timestamp instead of getLastEventTime() Widget.sendEvent(int, Event, boolean) and Display.sendEvent(int, Event) default a dispatched event's time to display.getLastEventTime() when the caller left Event.time at 0 (e.g. widget.notifyListeners(type, event) with a freshly-constructed Event, as commonly done by tests and some application code). On GTK, getLastEventTime() returns a field that is only updated while processing a real native GDK event; on Cocoa, it reads the timestamp of NSApplication.currentEvent(). Neither is updated by a synthetic event dispatched directly through notifyListeners()/sendEvent(), so two consecutive synthetic events issued without an intervening native event receive the exact same, stale time. Code that de-duplicates by (command, event time) - such as KeyBindingDispatcher's new duplicate key event guard - then incorrectly treats the second event as a repeat of the first and suppresses it. Win32's getLastEventTime() (OS.GetMessageTime()) is backed by the ever-advancing OS message-queue tick counter and does not exhibit this staleness, so it is left unchanged. Fix GTK and Cocoa by giving these synthetic events a fresh, monotonically-advancing timestamp instead, consistent with how Display.sendJDKInternalEvent() already timestamps its own synthetic events. Event.java itself (shared by all platforms) is untouched, as is any test code - this is purely a platform dispatch fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../cocoa/org/eclipse/swt/widgets/Display.java | 4 +++- .../cocoa/org/eclipse/swt/widgets/Widget.java | 12 +++++++++++- .../gtk/org/eclipse/swt/widgets/Display.java | 4 +++- .../gtk/org/eclipse/swt/widgets/Widget.java | 13 ++++++++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java index c2524f8e8c9..eeba3421a5e 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java @@ -4636,7 +4636,9 @@ void sendEvent (int eventType, Event event) { if (event == null) event = new Event (); event.display = this; event.type = eventType; - if (event.time == 0) event.time = getLastEventTime (); + // See comment in Widget.sendEvent(int, Event, boolean) for why + // getLastEventTime() is not used as a fallback here. + if (event.time == 0) event.time = (int) (System.nanoTime() / 1_000_000L); sendEvent (eventTable, event); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java index 3804a64ecf6..8d49f138b4c 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java @@ -1674,7 +1674,17 @@ void sendEvent (int eventType, Event event, boolean send) { event.display = display; event.widget = this; if (event.time == 0) { - event.time = display.getLastEventTime (); + /* + * display.getLastEventTime() only reflects the time of the last *native* + * NSEvent that was processed; it is not updated by synthetic events + * dispatched directly (e.g. notifyListeners()) without going through the + * native event queue. Relying on it here would make consecutive + * synthetic events (as commonly created by tests) carry the same, stale + * time, which can incorrectly look like a repeated/duplicate event to + * code (e.g. KeyBindingDispatcher) that de-duplicates by event time. + * Use a distinct, always-advancing timestamp instead. + */ + event.time = (int) (System.nanoTime() / 1_000_000L); } if (send) { sendEvent (event); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java index 4a1bc89cce3..769d255e887 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java @@ -5875,7 +5875,9 @@ void sendEvent (int eventType, Event event) { if (event == null) event = new Event (); event.display = this; event.type = eventType; - if (event.time == 0) event.time = getLastEventTime (); + // See comment in Widget.sendEvent(int, Event, boolean) for why + // getLastEventTime() is not used as a fallback here. + if (event.time == 0) event.time = (int) (System.nanoTime() / 1_000_000L); if (!filterEvent (event)) { if (eventTable != null) sendEvent (eventTable, event); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Widget.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Widget.java index 3471c7e010d..cee92f17042 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Widget.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Widget.java @@ -1679,7 +1679,18 @@ void sendEvent (int eventType, Event event, boolean send) { event.display = display; event.widget = this; if (event.time == 0) { - event.time = display.getLastEventTime (); + /* + * display.getLastEventTime() only reflects the time of the last *native* + * GDK event that was processed; it is not updated by synthetic events + * dispatched directly (e.g. notifyListeners()) without going through the + * native event queue. Relying on it here would make consecutive + * synthetic events (as commonly created by tests) carry the same, stale + * time, which can incorrectly look like a repeated/duplicate event to + * code (e.g. KeyBindingDispatcher) that de-duplicates by event time. + * Use a distinct, always-advancing timestamp instead, consistent with + * how other synthetic events (see sendJDKInternalEvent) are timestamped. + */ + event.time = (int) (System.nanoTime() / 1_000_000L); } if (send) { sendEvent (event);