Skip to content

Commit 8553d4d

Browse files
committed
Make Focus() reliable and let Blur() return focus to where it was
Focus() and Show() remember the window or application that had the focus; Blur() hands it back, or to the next window the platform would pick, and does nothing on a window without the focus (#69). - macOS: Focus() activates the app (unless non-activating) and deminiaturizes; Blur() keys the previous own window or yields activation to the previous app, falling back to the app this one last took the focus from. - Windows: Focus() restores a minimized window and retries through AttachThreadInput when the foreground lock refuses it, without stealing a child HWND's focus; Blur() gives the foreground to the recorded window or the next one down the Z order. - Linux: Blur() presents the previous own window; on X11 it activates the previous client's window, or the next one down _NET_CLIENT_LIST_STACKING, through _NET_ACTIVE_WINDOW as a pager. Regressions (change the active window; not in CTest): tests/window_focus_return_test (Windows, Linux X11, with a second process) and tests/window_focus_return_macos_test.
1 parent 3cc8b3d commit 8553d4d

8 files changed

Lines changed: 865 additions & 14 deletions

File tree

‎src/platform/linux/window_linux.cpp‎

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <iostream>
22
#include <algorithm>
33
#include <cmath>
4+
#include <memory>
45
#include <vector>
56
#include <mutex>
67
#include <unordered_map>
@@ -18,6 +19,7 @@
1819

1920
#ifdef GDK_WINDOWING_X11
2021
#include <gdk/gdkx.h>
22+
#include <X11/Xatom.h>
2123
#undef None // Xlib macro conflicts with VisualEffect::None.
2224
#endif
2325

@@ -887,16 +889,197 @@ WindowId Window::GetId() const {
887889

888890
static void PresentWindow(GtkWidget* widget);
889891

892+
// Where Blur() hands the keyboard back to: another toplevel of this process
893+
// (weakly held, so a destroyed one is not found again) or, on X11, the window
894+
// another client had active. Focus() and Show() record it on the GtkWindow so
895+
// every wrapper of that window shares it.
896+
static const char* kBlurReturnKey = "NativeAPIBlurReturn";
897+
struct BlurReturnTarget {
898+
GWeakRef own;
899+
unsigned long xid = 0;
900+
};
901+
902+
static void FreeBlurReturnTarget(gpointer data) {
903+
auto* target = static_cast<BlurReturnTarget*>(data);
904+
g_weak_ref_clear(&target->own);
905+
delete target;
906+
}
907+
908+
#ifdef GDK_WINDOWING_X11
909+
static unsigned long ActiveX11Window(GdkDisplay* display) {
910+
auto* xdisplay = GDK_DISPLAY_XDISPLAY(display);
911+
Atom type = 0;
912+
int format = 0;
913+
unsigned long count = 0, remaining = 0;
914+
unsigned char* data = nullptr;
915+
unsigned long active = 0;
916+
gdk_x11_display_error_trap_push(display);
917+
if (XGetWindowProperty(xdisplay, gdk_x11_get_default_root_xwindow(),
918+
gdk_x11_get_xatom_by_name_for_display(display, "_NET_ACTIVE_WINDOW"), 0, 1,
919+
False, XA_WINDOW, &type, &format, &count, &remaining, &data) == Success &&
920+
data && type == XA_WINDOW && format == 32 && count == 1) {
921+
active = *reinterpret_cast<unsigned long*>(data);
922+
}
923+
if (data) XFree(data);
924+
gdk_x11_display_error_trap_pop_ignored(display);
925+
return active;
926+
}
927+
#endif
928+
929+
static void RememberBlurReturnTarget(GtkWidget* widget) {
930+
if (!widget || !GTK_IS_WINDOW(widget)) return;
931+
// Already focused (a repeated Focus()/Show()): keep the earlier target.
932+
if (gtk_window_is_active(GTK_WINDOW(widget))) return;
933+
auto* target = new BlurReturnTarget();
934+
g_weak_ref_init(&target->own, nullptr);
935+
bool found = false;
936+
GList* toplevels = gtk_window_list_toplevels();
937+
for (GList* item = toplevels; item && !found; item = item->next) {
938+
if (item->data != widget && gtk_window_is_active(GTK_WINDOW(item->data))) {
939+
g_weak_ref_set(&target->own, item->data);
940+
found = true;
941+
}
942+
}
943+
g_list_free(toplevels);
944+
#ifdef GDK_WINDOWING_X11
945+
auto* display = gtk_widget_get_display(widget);
946+
if (!found && GDK_IS_X11_DISPLAY(display)) {
947+
auto* surface = gtk_widget_get_window(widget);
948+
const unsigned long active = ActiveX11Window(display);
949+
if (active && (!surface || active != GDK_WINDOW_XID(surface))) {
950+
target->xid = active;
951+
found = true;
952+
}
953+
}
954+
#endif
955+
if (!found) {
956+
FreeBlurReturnTarget(target);
957+
target = nullptr; // An unknown previous focus makes an older target stale.
958+
}
959+
g_object_set_data_full(G_OBJECT(widget), kBlurReturnKey, target,
960+
target ? FreeBlurReturnTarget : nullptr);
961+
}
962+
963+
#ifdef GDK_WINDOWING_X11
964+
// Asks the window manager to activate |xid| as a pager (source 2) would, which
965+
// focus stealing prevention lets through.
966+
static bool ActivateX11Window(GdkWindow* surface, unsigned long xid) {
967+
auto* display = gdk_window_get_display(surface);
968+
auto* xdisplay = GDK_DISPLAY_XDISPLAY(display);
969+
XWindowAttributes attributes;
970+
gdk_x11_display_error_trap_push(display);
971+
const bool viewable = XGetWindowAttributes(xdisplay, xid, &attributes) &&
972+
attributes.map_state == IsViewable;
973+
if (viewable) {
974+
XEvent event = {};
975+
event.xclient.type = ClientMessage;
976+
event.xclient.window = xid;
977+
event.xclient.message_type =
978+
gdk_x11_get_xatom_by_name_for_display(display, "_NET_ACTIVE_WINDOW");
979+
event.xclient.format = 32;
980+
event.xclient.data.l[0] = 2; // Source indication: pager.
981+
event.xclient.data.l[1] = gdk_x11_get_server_time(surface);
982+
event.xclient.data.l[2] = GDK_WINDOW_XID(surface);
983+
XSendEvent(xdisplay, gdk_x11_get_default_root_xwindow(), False,
984+
SubstructureRedirectMask | SubstructureNotifyMask, &event);
985+
XFlush(xdisplay);
986+
}
987+
return gdk_x11_display_error_trap_pop(display) == 0 && viewable;
988+
}
989+
990+
// The next window down the window manager's stacking order that a user could
991+
// switch to: viewable, not |own|, and not a dock, desktop or other helper.
992+
static unsigned long NextX11StackingWindow(GdkDisplay* display, unsigned long own) {
993+
auto* xdisplay = GDK_DISPLAY_XDISPLAY(display);
994+
const Atom normal = gdk_x11_get_xatom_by_name_for_display(display, "_NET_WM_WINDOW_TYPE_NORMAL");
995+
const Atom dialog = gdk_x11_get_xatom_by_name_for_display(display, "_NET_WM_WINDOW_TYPE_DIALOG");
996+
Atom type = 0;
997+
int format = 0;
998+
unsigned long count = 0, remaining = 0;
999+
unsigned char* data = nullptr;
1000+
unsigned long next = 0;
1001+
gdk_x11_display_error_trap_push(display);
1002+
if (XGetWindowProperty(xdisplay, gdk_x11_get_default_root_xwindow(),
1003+
gdk_x11_get_xatom_by_name_for_display(display, "_NET_CLIENT_LIST_STACKING"),
1004+
0, 4096, False, XA_WINDOW, &type, &format, &count, &remaining,
1005+
&data) == Success &&
1006+
data && type == XA_WINDOW && format == 32) {
1007+
const auto* windows = reinterpret_cast<unsigned long*>(data);
1008+
for (unsigned long i = count; i-- > 0 && !next;) { // Bottom to top: walk down.
1009+
const unsigned long candidate = windows[i];
1010+
XWindowAttributes attributes;
1011+
if (candidate == own || !XGetWindowAttributes(xdisplay, candidate, &attributes) ||
1012+
attributes.map_state != IsViewable)
1013+
continue;
1014+
Atom kind = normal; // EWMH: a window without a type is a normal one.
1015+
Atom kind_type = 0;
1016+
int kind_format = 0;
1017+
unsigned long kind_count = 0, kind_remaining = 0;
1018+
unsigned char* kind_data = nullptr;
1019+
if (XGetWindowProperty(xdisplay, candidate,
1020+
gdk_x11_get_xatom_by_name_for_display(display, "_NET_WM_WINDOW_TYPE"),
1021+
0, 1, False, XA_ATOM, &kind_type, &kind_format, &kind_count,
1022+
&kind_remaining, &kind_data) == Success &&
1023+
kind_data && kind_count == 1)
1024+
kind = *reinterpret_cast<Atom*>(kind_data);
1025+
if (kind_data) XFree(kind_data);
1026+
if (kind == normal || kind == dialog) next = candidate;
1027+
}
1028+
}
1029+
if (data) XFree(data);
1030+
gdk_x11_display_error_trap_pop_ignored(display);
1031+
return next;
1032+
}
1033+
#endif
1034+
1035+
// Gives the keyboard back to the recorded target. Wayland has no way to
1036+
// activate another client's surface, so there only an own window qualifies.
1037+
static bool ReturnFocusToTarget(GtkWidget* widget) {
1038+
auto* target = static_cast<BlurReturnTarget*>(
1039+
g_object_steal_data(G_OBJECT(widget), kBlurReturnKey));
1040+
if (!target) return false;
1041+
std::unique_ptr<BlurReturnTarget, void (*)(gpointer)> owned(target, FreeBlurReturnTarget);
1042+
if (auto* own = static_cast<GtkWidget*>(g_weak_ref_get(&target->own))) {
1043+
const bool usable = own != widget && gtk_widget_get_visible(own) &&
1044+
gtk_widget_get_window(own) &&
1045+
!(gdk_window_get_state(gtk_widget_get_window(own)) & GDK_WINDOW_STATE_ICONIFIED);
1046+
if (usable) PresentWindow(own);
1047+
g_object_unref(own);
1048+
if (usable) return true;
1049+
}
1050+
#ifdef GDK_WINDOWING_X11
1051+
auto* surface = gtk_widget_get_window(widget);
1052+
if (target->xid && surface && GDK_IS_X11_WINDOW(surface))
1053+
return ActivateX11Window(surface, target->xid);
1054+
#endif
1055+
return false;
1056+
}
1057+
8901058
void Window::Focus() {
8911059
if (!IsFocusable()) return;
8921060
if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) {
1061+
RememberBlurReturnTarget(pimpl_->widget_);
8931062
PresentWindow(pimpl_->widget_);
8941063
} else if (pimpl_->gdk_window_) {
8951064
gdk_window_focus(pimpl_->gdk_window_, GDK_CURRENT_TIME);
8961065
}
8971066
}
8981067

8991068
void Window::Blur() {
1069+
// Blurring a window without the keyboard focus must not move focus at all.
1070+
if (!IsFocused()) return;
1071+
if (pimpl_->widget_ && ReturnFocusToTarget(pimpl_->widget_)) return;
1072+
#ifdef GDK_WINDOWING_X11
1073+
if (pimpl_->gdk_window_ && GDK_IS_X11_WINDOW(pimpl_->gdk_window_)) {
1074+
// No usable target: the window below, as the window manager would pick
1075+
// when this one closed. With none, drop the keyboard focus in place.
1076+
auto* surface = pimpl_->gdk_window_;
1077+
const unsigned long next =
1078+
NextX11StackingWindow(gdk_window_get_display(surface), GDK_WINDOW_XID(surface));
1079+
if (!next || !ActivateX11Window(surface, next)) ReleaseKeyboardFocus(surface);
1080+
return;
1081+
}
1082+
#endif
9001083
if (pimpl_->gdk_window_) {
9011084
gdk_window_lower(pimpl_->gdk_window_);
9021085
}
@@ -940,6 +1123,7 @@ void Window::Show() {
9401123
gtk_window_deiconify(GTK_WINDOW(pimpl_->widget_));
9411124
ShowInactive();
9421125
} else if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) {
1126+
RememberBlurReturnTarget(pimpl_->widget_);
9431127
PresentWindow(pimpl_->widget_);
9441128
} else if (pimpl_->widget_) {
9451129
gtk_widget_show(pimpl_->widget_);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#pragma once
2+
3+
// Focus() / Blur() for an NSWindow, shared by every Window wrapper of it.
4+
//
5+
// Focus() and Show() remember where the keyboard focus was before they took
6+
// it, and Blur() hands it back: to the previous window of this app when that
7+
// one is still visible, otherwise to the previously active application. The
8+
// target lives on the NSWindow as an associated dictionary so this file works
9+
// under both ARC (Dart build hook) and manual reference counting (CMake). The
10+
// same-app window is kept by window number, never retained, so a closed window
11+
// is simply not found again.
12+
13+
static const void* kNativeApiFocusReturnTarget = &kNativeApiFocusReturnTarget;
14+
static NSString* const kNativeApiFocusReturnWindow = @"window";
15+
static NSString* const kNativeApiFocusReturnApplication = @"application";
16+
17+
// The application that was in front when any window of this app last took the
18+
// focus from it: the fallback when a window's own target is gone.
19+
static NSRunningApplication* g_native_api_last_other_application = nil;
20+
21+
static void NativeApiSetLastOtherApplication(NSRunningApplication* application) {
22+
#if !__has_feature(objc_arc)
23+
[application retain];
24+
[g_native_api_last_other_application release];
25+
#endif
26+
g_native_api_last_other_application = application;
27+
}
28+
29+
static void NativeApiForgetFocusReturnTarget(NSWindow* window) {
30+
objc_setAssociatedObject(window, kNativeApiFocusReturnTarget, nil,
31+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
32+
}
33+
34+
static void NativeApiRememberFocusReturnTarget(NSWindow* window) {
35+
if (!window || ![window canBecomeKeyWindow]) return;
36+
NSApplication* app = [NSApplication sharedApplication];
37+
NSRunningApplication* current = [NSRunningApplication currentApplication];
38+
NSRunningApplication* foreground = [[NSWorkspace sharedWorkspace] frontmostApplication];
39+
NSDictionary* target = nil;
40+
if (foreground && foreground.processIdentifier != current.processIdentifier) {
41+
target = @{kNativeApiFocusReturnApplication : foreground};
42+
NativeApiSetLastOtherApplication(foreground);
43+
} else {
44+
NSWindow* key = [app keyWindow];
45+
// Already focused (a repeated Focus()/Show()): keep the earlier target.
46+
if (!key || key == window) return;
47+
target = @{kNativeApiFocusReturnWindow : @([key windowNumber])};
48+
}
49+
objc_setAssociatedObject(window, kNativeApiFocusReturnTarget, target,
50+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
51+
}
52+
53+
static void NativeApiActivateApplication() {
54+
NSApplication* app = [NSApplication sharedApplication];
55+
if (@available(macOS 14.0, *)) {
56+
[app activate];
57+
} else {
58+
[app activateIgnoringOtherApps:YES];
59+
}
60+
}
61+
62+
static void NativeApiFocusWindow(NSWindow* window, bool non_activating) {
63+
if (!window || ![window canBecomeKeyWindow]) return;
64+
NativeApiRememberFocusReturnTarget(window);
65+
if ([window isMiniaturized]) [window deminiaturize:nil];
66+
// A nonactivating panel takes the keyboard without activating this app.
67+
if (!non_activating) NativeApiActivateApplication();
68+
[window makeKeyAndOrderFront:nil];
69+
}
70+
71+
static bool NativeApiActivateOtherApplication(NSRunningApplication* other) {
72+
NSRunningApplication* current = [NSRunningApplication currentApplication];
73+
if (!other || other.terminated || other.processIdentifier == current.processIdentifier)
74+
return false;
75+
// macOS 14+ activation is cooperative: yield explicitly first, or the other
76+
// app's request may be declined. Do not bring all of its windows forward.
77+
if (@available(macOS 14.0, *)) {
78+
[[NSApplication sharedApplication] yieldActivationToApplication:other];
79+
}
80+
return [other activateWithOptions:0];
81+
}
82+
83+
static void NativeApiBlurWindow(NSWindow* window) {
84+
// Blurring a window without the keyboard focus must not move focus at all.
85+
if (!window || ![window isKeyWindow]) return;
86+
NSApplication* app = [NSApplication sharedApplication];
87+
NSDictionary* target = objc_getAssociatedObject(window, kNativeApiFocusReturnTarget);
88+
NSNumber* number = target[kNativeApiFocusReturnWindow];
89+
NSRunningApplication* other = target[kNativeApiFocusReturnApplication];
90+
NativeApiForgetFocusReturnTarget(window);
91+
92+
if (number) {
93+
NSWindow* previous = [app windowWithWindowNumber:[number integerValue]];
94+
if (previous && previous != window && [previous isVisible] && ![previous isMiniaturized] &&
95+
[previous canBecomeKeyWindow]) {
96+
[previous makeKeyAndOrderFront:nil];
97+
return;
98+
}
99+
}
100+
if (![app isActive]) {
101+
// A key nonactivating panel while another app is active: give the
102+
// keyboard back to that app.
103+
other = [[NSWorkspace sharedWorkspace] frontmostApplication];
104+
}
105+
if (NativeApiActivateOtherApplication(other)) return;
106+
// No usable target of this window: the app this one took the focus from.
107+
if (NativeApiActivateOtherApplication(g_native_api_last_other_application)) return;
108+
// Nothing known. A plain deactivate does not reliably activate another app
109+
// under macOS 14+ cooperative activation, but it is all that is left.
110+
if ([app isActive]) [app deactivate];
111+
}

‎src/platform/macos/window_macos.mm‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ static void NativeApiUpdateWindowClass(NSWindow* window, bool non_activating) {
361361
}
362362

363363
#include "window_shadow_macos.h"
364+
#include "window_focus_macos.h"
364365

365366
namespace nativeapi {
366367

@@ -425,18 +426,20 @@ static void NativeApiUpdateWindowClass(NSWindow* window, bool non_activating) {
425426
}
426427

427428
void Window::Focus() {
428-
[pimpl_->ns_window_ makeKeyAndOrderFront:nil];
429+
if (!IsFocusable()) return;
430+
NativeApiFocusWindow(pimpl_->ns_window_, IsNonActivating());
429431
}
430432

431433
void Window::Blur() {
432-
[pimpl_->ns_window_ orderBack:nil];
434+
NativeApiBlurWindow(pimpl_->ns_window_);
433435
}
434436

435437
bool Window::IsFocused() const {
436438
return [pimpl_->ns_window_ isKeyWindow];
437439
}
438440

439441
void Window::Show() {
442+
if (IsFocusable()) NativeApiRememberFocusReturnTarget(pimpl_->ns_window_);
440443
[pimpl_->ns_window_ setIsVisible:YES];
441444
// Panels receive key focus when shown but should not activate the app.
442445
if (![pimpl_->ns_window_ isKindOfClass:[NSPanel class]]) {

0 commit comments

Comments
 (0)