diff --git a/meson.build b/meson.build index 6a310376e..709759f96 100644 --- a/meson.build +++ b/meson.build @@ -20,6 +20,7 @@ glib_req = '>= 2.69.0' gi_req = '>= 0.9.5' graphene_req = '>= 1.10.2' gtk3_req = '>= 3.19.8' +gtk4_req = '>= 4.0.0' gdk_pixbuf_req = '>= 2.0' uprof_req = '>= 0.3' pango_req = '>= 1.46.0' @@ -105,6 +106,7 @@ mutter_installed_tests_libexecdir = join_paths( m_dep = cc.find_library('m', required: true) graphene_dep = dependency('graphene-gobject-1.0', version: graphene_req) gtk3_dep = dependency('gtk+-3.0', version: gtk3_req) +gtk4_dep = dependency('gtk4', version: gtk4_req) gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0') pango_dep = dependency('pango', version: pango_req) cairo_dep = dependency('cairo', version: cairo_req) diff --git a/src/frames/main.c b/src/frames/main.c new file mode 100644 index 000000000..b6e429b4d --- /dev/null +++ b/src/frames/main.c @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#include "config.h" + +#include "meta-window-tracker.h" + +#include +#include +#include + +static gboolean +on_sigterm (gpointer user_data) +{ + GMainLoop *main_loop = user_data; + + g_main_loop_quit (main_loop); + + return G_SOURCE_REMOVE; +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr (MetaWindowTracker) window_tracker = NULL; + GdkDisplay *display; + GMainLoop *loop; + Display *xdisplay; + + /* We do know the desired GDK backend, don't let + * anyone tell us otherwise. + */ + g_unsetenv ("GDK_BACKEND"); + + gdk_set_allowed_backends ("x11"); + + g_set_prgname ("mutter-x11-frames"); + + gtk_init (); + + display = gdk_display_get_default (); + + xdisplay = gdk_x11_display_get_xdisplay (display); + XFixesSetClientDisconnectMode (xdisplay, + XFixesClientDisconnectFlagTerminate); + + window_tracker = meta_window_tracker_new (display); + + loop = g_main_loop_new (NULL, FALSE); + g_unix_signal_add (SIGTERM, on_sigterm, loop); + g_main_loop_run (loop); + g_main_loop_unref (loop); + + return 0; +} diff --git a/src/frames/meson.build b/src/frames/meson.build new file mode 100644 index 000000000..1ddccff23 --- /dev/null +++ b/src/frames/meson.build @@ -0,0 +1,24 @@ +x11_frames_sources = [ + 'main.c', + 'meta-frame.c', + 'meta-frame-content.c', + 'meta-frame-header.c', + 'meta-window-tracker.c', +] + +x11_frames = executable('mutter-x11-frames', + sources: x11_frames_sources, + dependencies: [ + gtk4_dep, + x11_dep, + xext_dep, + xfixes_dep, + xi_dep, + ], + c_args: [ + '-DG_LOG_DOMAIN="mutter-x11-frames"', + ], + include_directories: top_includepath, + install: true, + install_dir: get_option('libexecdir'), +) diff --git a/src/frames/meta-frame-content.c b/src/frames/meta-frame-content.c new file mode 100644 index 000000000..bd9965179 --- /dev/null +++ b/src/frames/meta-frame-content.c @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#include "config.h" + +#include "meta-frame-content.h" + +struct _MetaFrameContent +{ + GtkWidget parent_instance; + Window window; + GtkBorder border; +}; + +enum { + PROP_0, + PROP_XWINDOW, + PROP_BORDER, + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { 0, }; + +G_DEFINE_TYPE (MetaFrameContent, meta_frame_content, GTK_TYPE_WIDGET) + +static void +meta_frame_content_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MetaFrameContent *frame_content = META_FRAME_CONTENT (object); + + switch (prop_id) + { + case PROP_XWINDOW: + frame_content->window = (Window) g_value_get_ulong (value); + break; + case PROP_BORDER: + frame_content->border = *(GtkBorder*) g_value_get_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +meta_frame_content_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MetaFrameContent *frame_content = META_FRAME_CONTENT (object); + + switch (prop_id) + { + case PROP_XWINDOW: + g_value_set_ulong (value, (gulong) frame_content->window); + break; + case PROP_BORDER: + g_value_set_boxed (value, &frame_content->border); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +meta_frame_content_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + *minimum_baseline = *natural_baseline = -1; + *minimum = *natural = 1; +} + +static void +meta_frame_content_update_border (MetaFrameContent *content, + GtkBorder border) +{ + if (content->border.left == border.left && + content->border.right == border.right && + content->border.top == border.top && + content->border.bottom == border.bottom) + return; + + content->border = border; + g_object_notify (G_OBJECT (content), "border"); +} + +static void +meta_frame_content_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + MetaFrameContent *content = META_FRAME_CONTENT (widget); + GtkWindow *window = GTK_WINDOW (gtk_widget_get_root (widget)); + double x = 0, y = 0, scale; + + gtk_widget_translate_coordinates (widget, + GTK_WIDGET (window), + x, y, + &x, &y); + + scale = gdk_surface_get_scale_factor (gtk_native_get_surface (GTK_NATIVE (window))); + + meta_frame_content_update_border (content, + /* FIXME: right/bottom are broken, if they + * are ever other than 0. + */ + (GtkBorder) { + x * scale, 0, + y * scale, 0, + }); +} + +static void +meta_frame_content_class_init (MetaFrameContentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = meta_frame_content_set_property; + object_class->get_property = meta_frame_content_get_property; + + widget_class->measure = meta_frame_content_measure; + widget_class->size_allocate = meta_frame_content_size_allocate; + + props[PROP_XWINDOW] = g_param_spec_ulong ("xwindow", + "X window", + "X window", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB); + props[PROP_BORDER] = g_param_spec_boxed ("border", + "Border", + "Border", + GTK_TYPE_BORDER, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB); + + g_object_class_install_properties (object_class, + G_N_ELEMENTS (props), + props); +} + +static void +meta_frame_content_init (MetaFrameContent *content) +{ +} + +GtkWidget * +meta_frame_content_new (Window window) +{ + return g_object_new (META_TYPE_FRAME_CONTENT, + "xwindow", window, + NULL); +} + +Window +meta_frame_content_get_window (MetaFrameContent *content) +{ + return content->window; +} + +GtkBorder +meta_frame_content_get_border (MetaFrameContent *content) +{ + return content->border; +} diff --git a/src/frames/meta-frame-content.h b/src/frames/meta-frame-content.h new file mode 100644 index 000000000..6125ef931 --- /dev/null +++ b/src/frames/meta-frame-content.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#ifndef META_FRAME_CONTENT_H +#define META_FRAME_CONTENT_H + +#include + +#include + +#define META_TYPE_FRAME_CONTENT (meta_frame_content_get_type ()) +G_DECLARE_FINAL_TYPE (MetaFrameContent, meta_frame_content, + META, FRAME_CONTENT, GtkWidget) + +GtkWidget * meta_frame_content_new (Window window); + +Window meta_frame_content_get_window (MetaFrameContent *content); + +GtkBorder meta_frame_content_get_border (MetaFrameContent *content); + +#endif /* META_FRAME_CONTENT_H */ diff --git a/src/frames/meta-frame-header.c b/src/frames/meta-frame-header.c new file mode 100644 index 000000000..5f2cda1c5 --- /dev/null +++ b/src/frames/meta-frame-header.c @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#include "config.h" + +#include "meta-frame-header.h" + +struct _MetaFrameHeader +{ + GtkWidget parent_instance; +}; + +G_DEFINE_TYPE (MetaFrameHeader, meta_frame_header, GTK_TYPE_WIDGET) + +static void +meta_frame_header_dispose (GObject *object) +{ + GtkWidget *widget = GTK_WIDGET (object); + GtkWidget *child; + + child = gtk_widget_get_first_child (widget); + if (child) + gtk_widget_unparent (child); + + G_OBJECT_CLASS (meta_frame_header_parent_class)->dispose (object); +} + +static void +meta_frame_header_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + *minimum_baseline = *natural_baseline = -1; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + *minimum = *natural = 1; + } + else + { + GtkWidget *child; + + child = gtk_widget_get_first_child (widget); + gtk_widget_measure (child, + orientation, for_size, + minimum, natural, + minimum_baseline, + natural_baseline); + } +} + +static void +meta_frame_header_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtkWidget *child; + int minimum; + gboolean shrunk; + GtkAllocation child_allocation; + + child = gtk_widget_get_first_child (widget); + + gtk_widget_measure (child, + GTK_ORIENTATION_HORIZONTAL, + height, + &minimum, NULL, NULL, NULL); + + shrunk = width < minimum; + + child_allocation.x = shrunk ? width - minimum : 0; + child_allocation.y = 0; + child_allocation.width = shrunk ? minimum : width; + child_allocation.height = height; + + gtk_widget_size_allocate (child, &child_allocation, baseline); +} + +static void +meta_frame_header_class_init (MetaFrameHeaderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = meta_frame_header_dispose; + + widget_class->measure = meta_frame_header_measure; + widget_class->size_allocate = meta_frame_header_size_allocate; +} + +static void +meta_frame_header_init (MetaFrameHeader *content) +{ + GtkWidget *header_bar; + + header_bar = gtk_header_bar_new (); + gtk_widget_insert_before (header_bar, GTK_WIDGET (content), NULL); +} + +GtkWidget * +meta_frame_header_new (void) +{ + return g_object_new (META_TYPE_FRAME_HEADER, NULL); +} diff --git a/src/frames/meta-frame-header.h b/src/frames/meta-frame-header.h new file mode 100644 index 000000000..758fac8e7 --- /dev/null +++ b/src/frames/meta-frame-header.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#ifndef META_FRAME_HEADER_H +#define META_FRAME_HEADER_H + +#include + +#define META_TYPE_FRAME_HEADER (meta_frame_header_get_type ()) +G_DECLARE_FINAL_TYPE (MetaFrameHeader, meta_frame_header, + META, FRAME_HEADER, GtkWidget) + +GtkWidget * meta_frame_header_new (void); + +#endif /* META_FRAME_HEADER_H */ diff --git a/src/frames/meta-frame.c b/src/frames/meta-frame.c new file mode 100644 index 000000000..26d0e1f1a --- /dev/null +++ b/src/frames/meta-frame.c @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#include "config.h" + +#include "meta-frame.h" + +#include "meta-frame-content.h" +#include "meta-frame-header.h" + +#include +#include + +struct _MetaFrame +{ + GtkWindow parent_instance; + GtkWidget *content; +}; + +typedef struct +{ + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long input_mode; + unsigned long status; +} MotifWmHints; + +#define MWM_FUNC_ALL (1L << 0) +#define MWM_FUNC_RESIZE (1L << 1) +#define MWM_FUNC_MINIMIZE (1L << 3) +#define MWM_FUNC_MAXIMIZE (1L << 4) +#define MWM_FUNC_CLOSE (1L << 5) + +G_DEFINE_TYPE (MetaFrame, meta_frame, GTK_TYPE_WINDOW) + +static void +meta_frame_class_init (MetaFrameClass *klass) +{ +} + +static gboolean +on_frame_close_request (GtkWindow *window, + gpointer user_data) +{ + GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (window)); + GtkWidget *content; + XClientMessageEvent ev; + Window client_xwindow; + + content = gtk_window_get_child (window); + if (!content) + return FALSE; + + client_xwindow = + meta_frame_content_get_window (META_FRAME_CONTENT (content)); + + ev.type = ClientMessage; + ev.window = client_xwindow; + ev.message_type = + gdk_x11_get_xatom_by_name_for_display (display, "WM_PROTOCOLS"); + ev.format = 32; + ev.data.l[0] = + gdk_x11_get_xatom_by_name_for_display (display, "WM_DELETE_WINDOW"); + ev.data.l[1] = 0; /* FIXME: missing timestamp */ + + gdk_x11_display_error_trap_push (display); + XSendEvent (gdk_x11_display_get_xdisplay (display), + client_xwindow, False, 0, (XEvent*) &ev); + gdk_x11_display_error_trap_pop_ignored (display); + + return TRUE; +} + +static void +meta_frame_init (MetaFrame *frame) +{ + g_signal_connect (frame, "close-request", + G_CALLBACK (on_frame_close_request), NULL); +} + +static void +meta_frame_update_extents (MetaFrame *frame, + GtkBorder border) +{ + GtkWindow *window = GTK_WINDOW (frame); + GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (frame)); + GdkSurface *surface; + Window xframe; + unsigned long data[4]; + + surface = gtk_native_get_surface (GTK_NATIVE (window)); + if (!surface) + return; + + data[0] = border.left; + data[1] = border.right; + data[2] = border.top; + data[3] = border.bottom; + + xframe = gdk_x11_surface_get_xid (surface); + XChangeProperty (gdk_x11_display_get_xdisplay (display), + xframe, + gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_FRAME_EXTENTS"), + XA_CARDINAL, + 32, + PropModeReplace, + (guchar *) &data, 4); +} + +static void +on_border_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + MetaFrame *frame = user_data; + GtkWidget *content; + GtkBorder border; + + content = gtk_window_get_child (GTK_WINDOW (frame)); + border = meta_frame_content_get_border (META_FRAME_CONTENT (content)); + meta_frame_update_extents (frame, border); +} + +static void +frame_sync_title (GtkWindow *frame, + Window client_window) +{ + GdkDisplay *display; + char *title = NULL; + int format; + Atom type; + unsigned long nitems, bytes_after; + + display = gtk_widget_get_display (GTK_WIDGET (frame)); + + XGetWindowProperty (gdk_x11_display_get_xdisplay (display), + client_window, + gdk_x11_get_xatom_by_name_for_display (display, + "_NET_WM_NAME"), + 0, G_MAXLONG, False, + gdk_x11_get_xatom_by_name_for_display (display, + "UTF8_STRING"), + &type, &format, + &nitems, &bytes_after, + (unsigned char **) &title); + + gtk_window_set_title (frame, title); + g_free (title); +} + +static void +frame_sync_motif_wm_hints (GtkWindow *frame, + Window client_window) +{ + GdkDisplay *display; + MotifWmHints *mwm_hints = NULL; + int format; + Atom type; + unsigned long nitems, bytes_after; + gboolean deletable = TRUE; + + display = gtk_widget_get_display (GTK_WIDGET (frame)); + + XGetWindowProperty (gdk_x11_display_get_xdisplay (display), + client_window, + gdk_x11_get_xatom_by_name_for_display (display, + "_MOTIF_WM_HINTS"), + 0, sizeof (MotifWmHints) / sizeof (long), + False, AnyPropertyType, + &type, &format, + &nitems, &bytes_after, + (unsigned char **) &mwm_hints); + + if (mwm_hints) + { + if ((mwm_hints->functions & MWM_FUNC_ALL) == 0) + deletable = (mwm_hints->functions & MWM_FUNC_CLOSE) != 0; + else + deletable = (mwm_hints->functions & MWM_FUNC_CLOSE) == 0; + + g_free (mwm_hints); + } + + gtk_window_set_deletable (frame, deletable); +} + +static void +frame_sync_wm_normal_hints (GtkWindow *frame, + Window client_window) +{ + GdkDisplay *display; + XSizeHints size_hints; + long nitems; + gboolean resizable = TRUE; + + display = gtk_widget_get_display (GTK_WIDGET (frame)); + + XGetWMNormalHints (gdk_x11_display_get_xdisplay (display), + client_window, + &size_hints, + &nitems); + + if (nitems > 0) + { + resizable = ((size_hints.flags & PMinSize) == 0 || + (size_hints.flags & PMaxSize) == 0 || + size_hints.min_width != size_hints.max_width || + size_hints.min_height != size_hints.max_height); + } + + gtk_window_set_resizable (frame, resizable); +} + +GtkWidget * +meta_frame_new (Window window) +{ + GtkWidget *frame, *header, *content; + GdkSurface *surface; + int frame_height; + double scale; + + frame = g_object_new (META_TYPE_FRAME, NULL); + + header = meta_frame_header_new (); + + gtk_window_set_titlebar (GTK_WINDOW (frame), header); + + content = meta_frame_content_new (window); + gtk_window_set_child (GTK_WINDOW (frame), content); + + g_signal_connect (content, "notify::border", + G_CALLBACK (on_border_changed), frame); + + gtk_widget_realize (GTK_WIDGET (frame)); + surface = gtk_native_get_surface (GTK_NATIVE (frame)); + gdk_x11_surface_set_frame_sync_enabled (surface, FALSE); + + gtk_widget_measure (header, + GTK_ORIENTATION_VERTICAL, 1, + &frame_height, + NULL, NULL, NULL); + + scale = gdk_surface_get_scale_factor (gtk_native_get_surface (GTK_NATIVE (frame))); + + meta_frame_update_extents (META_FRAME (frame), + (GtkBorder) { + 0, 0, + frame_height * scale, 0, + }); + + frame_sync_title (GTK_WINDOW (frame), window); + frame_sync_motif_wm_hints (GTK_WINDOW (frame), window); + frame_sync_wm_normal_hints (GTK_WINDOW (frame), window); + + return frame; +} + +void +meta_frame_handle_xevent (MetaFrame *frame, + Window window, + XEvent *xevent) +{ + GdkDisplay *display; + GtkWidget *content; + gboolean is_frame, is_content; + GdkSurface *surface; + + surface = gtk_native_get_surface (GTK_NATIVE (frame)); + if (!surface) + return; + + content = gtk_window_get_child (GTK_WINDOW (frame)); + if (!content) + return; + + is_frame = window == gdk_x11_surface_get_xid (surface); + is_content = + window == meta_frame_content_get_window (META_FRAME_CONTENT (content)); + + if (!is_frame && !is_content) + return; + + display = gtk_widget_get_display (GTK_WIDGET (frame)); + + if (is_content && xevent->type == PropertyNotify) + { + if (xevent->xproperty.atom == + gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME")) + frame_sync_title (GTK_WINDOW (frame), xevent->xproperty.window); + else if (xevent->xproperty.atom == + gdk_x11_get_xatom_by_name_for_display (display, "_MOTIF_WM_HINTS")) + frame_sync_motif_wm_hints (GTK_WINDOW (frame), xevent->xproperty.window); + else if (xevent->xproperty.atom == + gdk_x11_get_xatom_by_name_for_display (display, "WM_NORMAL_HINTS")) + frame_sync_wm_normal_hints (GTK_WINDOW (frame), xevent->xproperty.window); + } +} diff --git a/src/frames/meta-frame.h b/src/frames/meta-frame.h new file mode 100644 index 000000000..d39dc8218 --- /dev/null +++ b/src/frames/meta-frame.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#ifndef META_FRAME_H +#define META_FRAME_H + +#include +#include + +#define META_TYPE_FRAME (meta_frame_get_type ()) +G_DECLARE_FINAL_TYPE (MetaFrame, meta_frame, META, FRAME, GtkWindow) + +GtkWidget * meta_frame_new (Window window); + +void meta_frame_handle_xevent (MetaFrame *frame, + Window window, + XEvent *xevent); + +#endif /* META_FRAME_CONTENT_H */ diff --git a/src/frames/meta-window-tracker.c b/src/frames/meta-window-tracker.c new file mode 100644 index 000000000..7487904c4 --- /dev/null +++ b/src/frames/meta-window-tracker.c @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#include "config.h" + +#include "meta-window-tracker.h" + +#include "meta-frame.h" + +#include +#include +#include + +struct _MetaWindowTracker +{ + GObject parent_instance; + GdkDisplay *display; + GHashTable *frames; + GHashTable *client_windows; + int xinput_opcode; +}; + +enum { + PROP_0, + PROP_DISPLAY, + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { 0, }; + +G_DEFINE_TYPE (MetaWindowTracker, meta_window_tracker, G_TYPE_OBJECT) + +static void +meta_window_tracker_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object); + + switch (prop_id) + { + case PROP_DISPLAY: + window_tracker->display = g_value_get_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +meta_window_tracker_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object); + + switch (prop_id) + { + case PROP_DISPLAY: + g_value_set_object (value, window_tracker->display); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +set_up_frame (MetaWindowTracker *window_tracker, + Window xwindow) +{ + GdkDisplay *display = window_tracker->display; + Display *xdisplay = gdk_x11_display_get_xdisplay (display); + GdkSurface *surface; + Window xframe; + unsigned long data[1]; + GtkWidget *frame; + + /* Double check it's not a request for a frame of our own. */ + if (g_hash_table_contains (window_tracker->frames, + GUINT_TO_POINTER (xwindow))) + return; + + /* Create a frame window */ + frame = meta_frame_new (xwindow); + surface = gtk_native_get_surface (GTK_NATIVE (frame)); + xframe = gdk_x11_surface_get_xid (surface); + + XAddToSaveSet (xdisplay, xwindow); + + data[0] = xwindow; + XChangeProperty (xdisplay, + xframe, + gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_FRAME_FOR"), + XA_WINDOW, + 32, + PropModeReplace, + (guchar *) data, 1); + + g_hash_table_insert (window_tracker->frames, + GUINT_TO_POINTER (xframe), frame); + g_hash_table_insert (window_tracker->client_windows, + GUINT_TO_POINTER (xwindow), frame); + gtk_widget_show (GTK_WIDGET (frame)); +} + +static void +listen_set_up_frame (MetaWindowTracker *window_tracker, + Window xwindow) +{ + GdkDisplay *display = window_tracker->display; + Display *xdisplay = gdk_x11_display_get_xdisplay (display); + int format; + Atom type; + unsigned long nitems, bytes_after; + unsigned char *data; + + gdk_x11_display_error_trap_push (display); + + XSelectInput (xdisplay, xwindow, + PropertyChangeMask | StructureNotifyMask); + + XGetWindowProperty (xdisplay, + xwindow, + gdk_x11_get_xatom_by_name_for_display (display, + "_MUTTER_NEEDS_FRAME"), + 0, 1, + False, XA_CARDINAL, + &type, &format, + &nitems, &bytes_after, + (unsigned char **) &data); + + if (gdk_x11_display_error_trap_pop (display)) + return; + + if (nitems > 0 && data[0]) + set_up_frame (window_tracker, xwindow); + + XFree (data); +} + +static void +remove_frame (MetaWindowTracker *window_tracker, + Window xwindow) +{ + GdkDisplay *display = window_tracker->display; + Display *xdisplay = gdk_x11_display_get_xdisplay (display); + GtkWidget *frame; + GdkSurface *surface; + Window xframe; + + frame = g_hash_table_lookup (window_tracker->client_windows, + GUINT_TO_POINTER (xwindow)); + if (!frame) + return; + + surface = gtk_native_get_surface (GTK_NATIVE (frame)); + xframe = gdk_x11_surface_get_xid (surface); + + gdk_x11_display_error_trap_push (display); + XRemoveFromSaveSet (xdisplay, xwindow); + gdk_x11_display_error_trap_pop_ignored (display); + + g_hash_table_remove (window_tracker->client_windows, + GUINT_TO_POINTER (xwindow)); + g_hash_table_remove (window_tracker->frames, + GUINT_TO_POINTER (xframe)); +} + +static gboolean +on_xevent (GdkDisplay *display, + XEvent *xevent, + gpointer user_data) +{ + Window xroot = gdk_x11_display_get_xrootwindow (display); + Window xwindow = xevent->xany.window; + MetaWindowTracker *window_tracker = user_data; + GtkWidget *frame; + + if (xevent->type == CreateNotify && + xevent->xcreatewindow.parent == xroot && + !xevent->xcreatewindow.override_redirect && + !g_hash_table_contains (window_tracker->frames, + GUINT_TO_POINTER (xevent->xcreatewindow.window))) + { + xwindow = xevent->xcreatewindow.window; + listen_set_up_frame (window_tracker, xwindow); + } + else if (xevent->type == DestroyNotify) + { + xwindow = xevent->xdestroywindow.window; + remove_frame (window_tracker, xwindow); + } + else if (xevent->type == PropertyNotify && + xevent->xproperty.atom == + gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_NEEDS_FRAME")) + { + if (xevent->xproperty.state == PropertyNewValue) + set_up_frame (window_tracker, xwindow); + else if (xevent->xproperty.state == PropertyDelete) + remove_frame (window_tracker, xwindow); + } + else if (xevent->type == PropertyNotify) + { + frame = g_hash_table_lookup (window_tracker->frames, + GUINT_TO_POINTER (xwindow)); + + if (!frame) + { + frame = g_hash_table_lookup (window_tracker->client_windows, + GUINT_TO_POINTER (xwindow)); + } + + if (frame) + meta_frame_handle_xevent (META_FRAME (frame), xwindow, xevent); + } + else if (xevent->type == GenericEvent && + xevent->xcookie.extension == window_tracker->xinput_opcode) + { + Display *xdisplay = gdk_x11_display_get_xdisplay (display); + XIEvent *xi_event; + + xi_event = (XIEvent *) xevent->xcookie.data; + + if (xi_event->evtype == XI_Leave) + { + XILeaveEvent *crossing = (XILeaveEvent *) xi_event; + + xwindow = crossing->event; + frame = g_hash_table_lookup (window_tracker->frames, + GUINT_TO_POINTER (xwindow)); + + /* When crossing from the frame to the client + * window, we may need to restore the cursor to + * its default. + */ + if (frame && crossing->detail == XINotifyInferior) + XIUndefineCursor (xdisplay, crossing->deviceid, xwindow); + } + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +query_xi_extension (MetaWindowTracker *window_tracker, + Display *xdisplay) +{ + int major = 2, minor = 3; + int unused; + + if (XQueryExtension (xdisplay, + "XInputExtension", + &window_tracker->xinput_opcode, + &unused, + &unused)) + { + if (XIQueryVersion (xdisplay, &major, &minor) == Success) + return TRUE; + } + + return FALSE; +} + +static void +meta_window_tracker_constructed (GObject *object) +{ + MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object); + GdkDisplay *display = window_tracker->display; + Display *xdisplay = gdk_x11_display_get_xdisplay (display); + Window xroot = gdk_x11_display_get_xrootwindow (display); + Window *windows, ignored1, ignored2; + unsigned int i, n_windows; + + G_OBJECT_CLASS (meta_window_tracker_parent_class)->constructed (object); + + query_xi_extension (window_tracker, xdisplay); + + XSelectInput (xdisplay, xroot, + KeyPressMask | + PropertyChangeMask); + + g_signal_connect (display, "xevent", + G_CALLBACK (on_xevent), object); + + gdk_x11_display_error_trap_push (display); + + XQueryTree (xdisplay, + xroot, + &ignored1, &ignored2, + &windows, &n_windows); + + if (gdk_x11_display_error_trap_pop (display)) + { + g_warning ("Could not query existing windows"); + return; + } + + for (i = 0; i < n_windows; i++) + { + XWindowAttributes attrs; + + gdk_x11_display_error_trap_push (display); + + XGetWindowAttributes (xdisplay, + windows[i], + &attrs); + + if (gdk_x11_display_error_trap_pop (display)) + continue; + + if (attrs.override_redirect) + continue; + + listen_set_up_frame (window_tracker, windows[i]); + } + + XFree (windows); +} + +static void +meta_window_tracker_finalize (GObject *object) +{ + MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object); + + g_clear_pointer (&window_tracker->frames, + g_hash_table_unref); + g_clear_pointer (&window_tracker->client_windows, + g_hash_table_unref); + + G_OBJECT_CLASS (meta_window_tracker_parent_class)->finalize (object); +} + +static void +meta_window_tracker_class_init (MetaWindowTrackerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = meta_window_tracker_set_property; + object_class->get_property = meta_window_tracker_get_property; + object_class->constructed = meta_window_tracker_constructed; + object_class->finalize = meta_window_tracker_finalize; + + props[PROP_DISPLAY] = g_param_spec_object ("display", + "Display", + "Display", + GDK_TYPE_DISPLAY, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB); + + g_object_class_install_properties (object_class, + G_N_ELEMENTS (props), + props); +} + +static void +meta_window_tracker_init (MetaWindowTracker *window_tracker) +{ + window_tracker->frames = + g_hash_table_new_full (NULL, NULL, NULL, + (GDestroyNotify) gtk_window_destroy); + window_tracker->client_windows = g_hash_table_new (NULL, NULL); +} + +MetaWindowTracker * +meta_window_tracker_new (GdkDisplay *display) +{ + return g_object_new (META_TYPE_WINDOW_TRACKER, + "display", display, + NULL); +} diff --git a/src/frames/meta-window-tracker.h b/src/frames/meta-window-tracker.h new file mode 100644 index 000000000..4b52b33e9 --- /dev/null +++ b/src/frames/meta-window-tracker.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Carlos Garnacho + */ + +#ifndef META_WINDOW_TRACKER_H +#define META_WINDOW_TRACKER_H + +#include +#include + +#define META_TYPE_WINDOW_TRACKER (meta_window_tracker_get_type ()) +G_DECLARE_FINAL_TYPE (MetaWindowTracker, meta_window_tracker, + META, WINDOW_TRACKER, + GObject) + +MetaWindowTracker * meta_window_tracker_new (GdkDisplay *display); + +#endif /* META_WINDOW_TRACKER_H */ diff --git a/src/meson.build b/src/meson.build index 91cf29adb..6a995f3d4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1189,6 +1189,7 @@ pkg.generate(libmutter, ) subdir('compositor/plugins') +subdir('frames') if have_core_tests subdir('tests')