diff --git a/.gitignore b/.gitignore index 81a262cdd..298177b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ mutter-message mutter-window-demo focus-window test-attached +test-focus test-gravity test-resizing test-size-hints diff --git a/src/wm-tester/Makefile.am b/src/wm-tester/Makefile.am index 1229f197b..2bb29d96e 100644 --- a/src/wm-tester/Makefile.am +++ b/src/wm-tester/Makefile.am @@ -19,7 +19,10 @@ test_size_hints_SOURCES= \ test_attached_SOURCES= \ test-attached.c -noinst_PROGRAMS=wm-tester test-gravity test-resizing focus-window test-size-hints test-attached +test_focus_SOURCES= \ + test-focus.c + +noinst_PROGRAMS=wm-tester test-gravity test-resizing focus-window test-size-hints test-attached test-focus wm_tester_LDADD= @MUTTER_LIBS@ test_gravity_LDADD= @MUTTER_LIBS@ @@ -27,3 +30,4 @@ test_resizing_LDADD= @MUTTER_LIBS@ test_size_hints_LDADD= @MUTTER_LIBS@ focus_window_LDADD= @MUTTER_LIBS@ test_attached_LDADD= @MUTTER_LIBS@ +test_focus_LDADD= @MUTTER_LIBS@ diff --git a/src/wm-tester/test-focus.c b/src/wm-tester/test-focus.c new file mode 100644 index 000000000..2ff59c5a5 --- /dev/null +++ b/src/wm-tester/test-focus.c @@ -0,0 +1,362 @@ +#include +#include +#include + +GtkWidget *main_window; +GtkWidget *noinput_window, *passive_window, *local_window; +GtkWidget *global_window, *lame_window, *grabby_window, *dying_window; + +static void +clear_on_destroy (GtkWidget *widget, gpointer user_data) +{ + GtkWidget **widget_pointer = user_data; + + *widget_pointer = NULL; +} + +static void +disable_take_focus (GtkWidget *window) +{ + GdkDisplay *display; + GdkWindow *gdkwindow; + Atom *protocols, wm_take_focus; + int n_protocols, i; + + gtk_widget_realize (window); + gdkwindow = gtk_widget_get_window (window); + display = gdk_window_get_display (gdkwindow); + + wm_take_focus = gdk_x11_get_xatom_by_name_for_display (display, "WM_TAKE_FOCUS"); + XGetWMProtocols (GDK_DISPLAY_XDISPLAY (display), + GDK_WINDOW_XID (gdkwindow), + &protocols, &n_protocols); + for (i = 0; i < n_protocols; i++) + { + if (protocols[i] == wm_take_focus) + { + protocols[i] = protocols[n_protocols - 1]; + n_protocols--; + break; + } + } + XSetWMProtocols (GDK_DISPLAY_XDISPLAY (display), + GDK_WINDOW_XID (gdkwindow), + protocols, n_protocols); + XFree (protocols); +} + +static void +clear_input_hint (GtkWidget *window) +{ + /* This needs to be called after gtk_widget_show, otherwise + * GTK+ will overwrite it. */ + GdkWindow *gdkwindow = gtk_widget_get_window (window); + XWMHints *wm_hints; + + wm_hints = XGetWMHints (GDK_DISPLAY_XDISPLAY (gdk_window_get_display (gdkwindow)), + GDK_WINDOW_XID (gdkwindow)); + + wm_hints->flags |= InputHint; + wm_hints->input = False; + + XSetWMHints (GDK_DISPLAY_XDISPLAY (gdk_window_get_display (gdkwindow)), + GDK_WINDOW_XID (gdkwindow), + wm_hints); + + XFree (wm_hints); +} + +static void +active_notify (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + GtkLabel *label = user_data; + + if (gtk_window_is_active (GTK_WINDOW (obj))) + gtk_label_set_text (label, "Focused"); + else + gtk_label_set_text (label, "Not focused"); +} + +static void +make_focused_label (GtkWidget *toplevel, + GtkWidget *parent) +{ + GtkWidget *label; + + label = gtk_label_new (""); + gtk_widget_show (label); + + gtk_container_add (GTK_CONTAINER (parent), label); + + g_signal_connect (toplevel, "notify::is-active", + G_CALLBACK (active_notify), label); + active_notify (G_OBJECT (toplevel), NULL, label); +} + +static void +setup_test_dialog (GtkWidget *toplevel) +{ + make_focused_label (toplevel, toplevel); + gtk_widget_set_size_request (toplevel, 200, 200); +} + +static void +noinput_clicked (GtkButton *button, gpointer user_data) +{ + if (noinput_window) + gtk_window_present_with_time (GTK_WINDOW (noinput_window), gtk_get_current_event_time ()); + else + { + noinput_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "No Input", + "accept-focus", FALSE, + NULL); + setup_test_dialog (noinput_window); + g_signal_connect (noinput_window, "destroy", + G_CALLBACK (clear_on_destroy), &noinput_window); + disable_take_focus (noinput_window); + gtk_widget_show (noinput_window); + } +} + +static void +passive_clicked (GtkButton *button, gpointer user_data) +{ + if (passive_window) + gtk_window_present_with_time (GTK_WINDOW (passive_window), gtk_get_current_event_time ()); + else + { + passive_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "Passive Input", + "accept-focus", TRUE, + NULL); + setup_test_dialog (passive_window); + g_signal_connect (passive_window, "destroy", + G_CALLBACK (clear_on_destroy), &passive_window); + disable_take_focus (passive_window); + gtk_widget_show (passive_window); + } +} + +static void +local_clicked (GtkButton *button, gpointer user_data) +{ + if (local_window) + gtk_window_present_with_time (GTK_WINDOW (local_window), gtk_get_current_event_time ()); + else + { + local_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "Locally Active Input", + "accept-focus", TRUE, + NULL); + setup_test_dialog (local_window); + g_signal_connect (local_window, "destroy", + G_CALLBACK (clear_on_destroy), &local_window); + gtk_widget_show (local_window); + } +} + +static void +global_clicked (GtkButton *button, gpointer user_data) +{ + if (global_window) + gtk_window_present_with_time (GTK_WINDOW (global_window), gtk_get_current_event_time ()); + else + { + /* gtk will only process WM_TAKE_FOCUS messages if accept-focus + * is TRUE. So we set that property and then manually clear the + * Input WMHint. + */ + global_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "Globally Active Input", + "accept-focus", TRUE, + NULL); + setup_test_dialog (global_window); + g_signal_connect (global_window, "destroy", + G_CALLBACK (clear_on_destroy), &global_window); + gtk_widget_show (global_window); + clear_input_hint (global_window); + } +} + +static void +lame_clicked (GtkButton *button, gpointer user_data) +{ + if (lame_window) + gtk_window_present_with_time (GTK_WINDOW (lame_window), gtk_get_current_event_time ()); + else + { + lame_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "Lame Globally Active Input", + "accept-focus", FALSE, + NULL); + setup_test_dialog (lame_window); + g_signal_connect (lame_window, "destroy", + G_CALLBACK (clear_on_destroy), &lame_window); + gtk_widget_show (lame_window); + } +} + +static void +grabby_active_changed (GObject *object, GParamSpec *param, gpointer user_data) +{ + if (gtk_window_is_active (GTK_WINDOW (grabby_window))) + { + GdkWindow *gdkwindow = gtk_widget_get_window (grabby_window); + guint32 now = gdk_x11_get_server_time (gdkwindow); + + gtk_window_present_with_time (GTK_WINDOW (main_window), now - 1); + XSetInputFocus (GDK_DISPLAY_XDISPLAY (gdk_window_get_display (gdkwindow)), + GDK_WINDOW_XID (gdkwindow), + RevertToParent, + now); + } +} + +static void +grabby_clicked (GtkButton *button, gpointer user_data) +{ + if (grabby_window) + gtk_window_present_with_time (GTK_WINDOW (grabby_window), gtk_get_current_event_time ()); + else + { + grabby_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "Focus-grabbing Window", + "accept-focus", TRUE, + /* Because mutter maps windows + * asynchronously, our trick won't + * work if we try to do it when the + * window is first mapped. + */ + "focus-on-map", FALSE, + NULL); + setup_test_dialog (grabby_window); + g_signal_connect (grabby_window, "destroy", + G_CALLBACK (clear_on_destroy), &grabby_window); + g_signal_connect (grabby_window, "notify::is-active", + G_CALLBACK (grabby_active_changed), NULL); + gtk_widget_show (grabby_window); + } +} + +static void +dying_clicked (GtkButton *button, gpointer user_data) +{ + if (dying_window) + { + gtk_window_present_with_time (GTK_WINDOW (dying_window), gtk_get_current_event_time ()); + gtk_widget_destroy (dying_window); + } + else + { + GtkWidget *label; + + dying_window = g_object_new (GTK_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + "title", "Dying Window", + "accept-focus", TRUE, + /* As with grabby window */ + "focus-on-map", FALSE, + NULL); + setup_test_dialog (dying_window); + g_signal_connect (dying_window, "destroy", + G_CALLBACK (clear_on_destroy), &dying_window); + + label = gtk_label_new ("Click button again to test"); + gtk_container_add (GTK_CONTAINER (dying_window), label); + gtk_widget_set_size_request (dying_window, 200, 200); + + gtk_widget_show_all (dying_window); + } +} + +static void +main_window_destroyed (GtkWidget *widget, gpointer user_data) +{ + gtk_main_quit (); +} + +int +main (int argc, char **argv) +{ + GtkWidget *vbox, *button; + + gtk_init (&argc, &argv); + + main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (main_window), "Focus Tester"); + + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); + gtk_box_set_homogeneous (GTK_BOX (vbox), 8); + gtk_container_set_border_width (GTK_CONTAINER (vbox), 8); + gtk_container_add (GTK_CONTAINER (main_window), vbox); + + make_focused_label (main_window, vbox); + + /* ICCCM "No Input" mode; Input hint False, WM_TAKE_FOCUS absent */ + button = gtk_button_new_with_label ("No Input Window"); + g_signal_connect (button, "clicked", G_CALLBACK (noinput_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + /* ICCCM "Passive" mode; Input hint True, WM_TAKE_FOCUS absent */ + button = gtk_button_new_with_label ("Passive Input Window"); + g_signal_connect (button, "clicked", G_CALLBACK (passive_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + /* ICCCM "Locally Active" mode; Input hint True, WM_TAKE_FOCUS present. + * This is the behavior of GtkWindows with accept_focus==TRUE. + */ + button = gtk_button_new_with_label ("Locally Active Window"); + g_signal_connect (button, "clicked", G_CALLBACK (local_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + /* ICCCM "Globally Active" mode; Input hint False, WM_TAKE_FOCUS present, + * and the window responds to WM_TAKE_FOCUS by calling XSetInputFocus. + */ + button = gtk_button_new_with_label ("Globally Active Window"); + g_signal_connect (button, "clicked", G_CALLBACK (global_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + /* "Lame" Globally Active mode; like "Globally Active", except that + * the window does not respond to WM_TAKE_FOCUS. This is the + * behavior of GtkWindows with accept_focus==FALSE. + */ + button = gtk_button_new_with_label ("Globally Lame Window"); + g_signal_connect (button, "clicked", G_CALLBACK (lame_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + /* "Grabby" window; when you activate the window, it asks the wm to + * focus the main window, but then forcibly grabs focus back with + * a newer timestamp, causing the wm to be temporarily confused + * about what window is focused and triggering the "Earlier attempt + * to focus ... failed" codepath. + */ + button = gtk_button_new_with_label ("Grabby Window"); + g_signal_connect (button, "clicked", G_CALLBACK (grabby_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + /* "Dying" window; we create the window on the first click, then + * activate it and destroy it on the second click, causing mutter to + * do an XSetInputFocus but not receive the corresponding FocusIn. + */ + button = gtk_button_new_with_label ("Dying Window"); + g_signal_connect (button, "clicked", G_CALLBACK (dying_clicked), NULL); + gtk_container_add (GTK_CONTAINER (vbox), button); + + gtk_widget_show_all (main_window); + + g_signal_connect (main_window, "destroy", + G_CALLBACK (main_window_destroyed), NULL); + + gtk_main (); + + return 0; +}