|
1 | 1 | #include <iostream> |
2 | 2 | #include <algorithm> |
3 | 3 | #include <cmath> |
| 4 | +#include <memory> |
4 | 5 | #include <vector> |
5 | 6 | #include <mutex> |
6 | 7 | #include <unordered_map> |
|
18 | 19 |
|
19 | 20 | #ifdef GDK_WINDOWING_X11 |
20 | 21 | #include <gdk/gdkx.h> |
| 22 | +#include <X11/Xatom.h> |
21 | 23 | #undef None // Xlib macro conflicts with VisualEffect::None. |
22 | 24 | #endif |
23 | 25 |
|
@@ -887,16 +889,197 @@ WindowId Window::GetId() const { |
887 | 889 |
|
888 | 890 | static void PresentWindow(GtkWidget* widget); |
889 | 891 |
|
| 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 | + |
890 | 1058 | void Window::Focus() { |
891 | 1059 | if (!IsFocusable()) return; |
892 | 1060 | if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) { |
| 1061 | + RememberBlurReturnTarget(pimpl_->widget_); |
893 | 1062 | PresentWindow(pimpl_->widget_); |
894 | 1063 | } else if (pimpl_->gdk_window_) { |
895 | 1064 | gdk_window_focus(pimpl_->gdk_window_, GDK_CURRENT_TIME); |
896 | 1065 | } |
897 | 1066 | } |
898 | 1067 |
|
899 | 1068 | 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 |
900 | 1083 | if (pimpl_->gdk_window_) { |
901 | 1084 | gdk_window_lower(pimpl_->gdk_window_); |
902 | 1085 | } |
@@ -940,6 +1123,7 @@ void Window::Show() { |
940 | 1123 | gtk_window_deiconify(GTK_WINDOW(pimpl_->widget_)); |
941 | 1124 | ShowInactive(); |
942 | 1125 | } else if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) { |
| 1126 | + RememberBlurReturnTarget(pimpl_->widget_); |
943 | 1127 | PresentWindow(pimpl_->widget_); |
944 | 1128 | } else if (pimpl_->widget_) { |
945 | 1129 | gtk_widget_show(pimpl_->widget_); |
|
0 commit comments