[mutter/wip/carlosg/frames-client: 995/1005] frames: Add new X11 frames client




commit c62b7f8590e07b40988d3377f71fca0a112d24cb
Author: Carlos Garnacho <carlosg gnome org>
Date:   Tue Dec 21 21:11:05 2021 +0100

    frames: Add new X11 frames client
    
    This separate X11 client takes care of creating frames for client
    windows, Mutter will use this client to delegate window frame
    rendering and event handling.
    
    This client is itself a GTK4 application, using real GtkWindows to
    implement window frames, the client window is expected to be
    reparented by the WM (still Mutter) into the GtkWindow X11 Window,
    and picked up from there.
    
    In order to make things behave, the frames need to forward some
    messaging to client windows (e.g. WM_DELETE_WINDOW when the X button
    is pressed), and some things it needs to read and synchronize with
    the client (e.g. window titles).
    
    Other than that, this is still a CSD GTK4 window, so window drags
    and resizes, and window context menus are forwarded for the WM to
    handle.

 meson.build                      |   2 +
 src/frames/main.c                |  58 +++++++
 src/frames/meson.build           |  23 +++
 src/frames/meta-frame-content.c  | 199 +++++++++++++++++++++++
 src/frames/meta-frame-content.h  |  37 +++++
 src/frames/meta-frame-header.c   | 125 +++++++++++++++
 src/frames/meta-frame-header.h   |  31 ++++
 src/frames/meta-frame.c          | 282 +++++++++++++++++++++++++++++++++
 src/frames/meta-frame.h          |  35 +++++
 src/frames/meta-window-tracker.c | 332 +++++++++++++++++++++++++++++++++++++++
 src/frames/meta-window-tracker.h |  33 ++++
 src/meson.build                  |   1 +
 12 files changed, 1158 insertions(+)
---
diff --git a/meson.build b/meson.build
index d3c2aeabcb..8ff8ca4343 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 0000000000..141ba41b46
--- /dev/null
+++ b/src/frames/main.c
@@ -0,0 +1,58 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-window-tracker.h"
+
+#include <gdk/x11/gdkx.h>
+#include <X11/extensions/Xfixes.h>
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_autoptr (MetaWindowTracker) window_tracker = NULL;
+  GdkDisplay *display;
+  GMainLoop *loop;
+  Display *xdisplay;
+
+  /* This seems to be the renderer that works best with
+   * frame sync disabled.
+   */
+  g_setenv ("GSK_RENDERER", "cairo", TRUE);
+
+  gdk_set_allowed_backends ("x11");
+
+  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_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 0000000000..e80ac83b60
--- /dev/null
+++ b/src/frames/meson.build
@@ -0,0 +1,23 @@
+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,
+  ],
+  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 0000000000..bd9965179a
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#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 0000000000..6125ef931b
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_FRAME_CONTENT_H
+#define META_FRAME_CONTENT_H
+
+#include <gtk/gtk.h>
+
+#include <X11/Xlib.h>
+
+#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 0000000000..5f2cda1c5b
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#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 0000000000..758fac8e75
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_FRAME_HEADER_H
+#define META_FRAME_HEADER_H
+
+#include <gtk/gtk.h>
+
+#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 0000000000..190e59fd8d
--- /dev/null
+++ b/src/frames/meta-frame.c
@@ -0,0 +1,282 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-frame.h"
+
+#include "meta-frame-content.h"
+#include "meta-frame-header.h"
+
+#include <gdk/x11/gdkx.h>
+#include <X11/Xatom.h>
+
+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);
+}
+
+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);
+
+  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);
+    }
+}
diff --git a/src/frames/meta-frame.h b/src/frames/meta-frame.h
new file mode 100644
index 0000000000..d39dc8218d
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_FRAME_H
+#define META_FRAME_H
+
+#include <gtk/gtk.h>
+#include <X11/Xlib.h>
+
+#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 0000000000..1330325292
--- /dev/null
+++ b/src/frames/meta-window-tracker.c
@@ -0,0 +1,332 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-window-tracker.h"
+
+#include "meta-frame.h"
+
+#include <gdk/x11/gdkx.h>
+#include <X11/Xatom.h>
+
+struct _MetaWindowTracker
+{
+  GObject parent_instance;
+  GdkDisplay *display;
+  GHashTable *frames;
+  GHashTable *client_windows;
+};
+
+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);
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+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);
+
+  XSelectInput (xdisplay, xroot,
+                KeyPressMask |
+                PropertyChangeMask);
+
+  g_signal_connect (display, "xevent",
+                    G_CALLBACK (on_xevent), object);
+
+  XQueryTree (xdisplay,
+              xroot,
+              &ignored1, &ignored2,
+              &windows, &n_windows);
+
+  for (i = 0; i < n_windows; i++)
+    {
+      XWindowAttributes attrs;
+
+      XGetWindowAttributes (xdisplay,
+                            windows[i],
+                            &attrs);
+
+      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 0000000000..4b52b33e9b
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_WINDOW_TRACKER_H
+#define META_WINDOW_TRACKER_H
+
+#include <gtk/gtk.h>
+#include <X11/Xlib.h>
+
+#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 5fa5f5f9d0..02c4e3a3ca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1187,6 +1187,7 @@ pkg.generate(libmutter,
 )
 
 subdir('compositor/plugins')
+subdir('frames')
 
 if have_core_tests
   subdir('tests')


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]